Search code examples
databasewordpressworkflowmultisite

WordPress multisite and single database for all developers


I'm in the process of planning a development team workflow for a WordPress multisite setup. We've chosen multisite because we want to limit the update/upgrade process and because the vast majority of our clients have the same website structure/requirements as far as the back-end goes. For the front-end, we're using a 'vanilla' theme that contains our css/js frameworks and the main settings, post types, options etc and we will be building all our websites based on our vanilla theme and extending it for each client via child themes. I've decided to keep everything under version control (Git), even WordPress core files.

As far as our current development environment: instead of a LAMP stack or a VVV/Docker setup, we've been using a centralized web server and each developer accesses his projects via a url that maps to his local repo by using a separate vhost per project (site). So, when working on projectA, John's url is http://john.projectA.dev, whereas Jack's url is http://jack.projectA.dev.

Using the same DB for all developers initially seemed like the 'logical' way to go in order to ensure data consistency, common settings etc but I've not been able to tie the ends yet. Due to the way WordPress stores the siteurl and homeurl URLs in the wp_options table, I've failed to find a way to map John to the site he'll be working on via URL, or to allow all developers access the common multisite's wp-admin which defaults to one of the user's URL (ie http://bob.wpmulisite.dev where Bob is the developer who initially set up the multisite environment. I tried overriding the urls by defining them in custom wp-configs per developer but that didn't go far as the WP_HOME and WP_SITEURL are ignored in WP multisite.

I don't like the idea of having to work with db dumps, replacing urls in the dump and importing it back into a database for each user because I fear that the process will soon lead to problems and it will inflict too much merging, diff-ing etc. However, I may be wrong and I'm absolutely open to any suggestions.

I've outlined my main problem above, if you find it unclear I'll be happy in rephrasing it. At this point I'm not very sure of what level of detail you might be interested in so just ask me and I'll dig in.


Solution

  • Ok, let me take you on a journey.

    Your centralized dev environment sounds very unique, but I think we can work with that. I have a similar system set up: we have a "staging" server with a public domain/ip for clients to view, and each developer has a full copy via git on their local machine with a local Apache server. We point our local dev sites to the central staging server database so that content/settings are consistent. We access our local dev sites with a custom local domain like http://example.local, the staging site is http://example.staging.com, and the live site will be http://example.com.

    As an aside, we have wp-config.php in .gitignore, so we can each customize it on our local copy. Usually we keep the staging and live versions as differently-named files like wp-config-live.php and symlink it to wp-config.php on the live server, etc., so that we can also keep config settings in our private git repo.

    Now, we want to work on our local copies of the multisite at example.local and also access its network sites via subfolders (example.local/site2/) or other custom domains we have aliased in vhost settings (example2.local) on each computer. Also, the staging site should work without anything more than a git pull on the server. On top of that, the live site should also work with a simple git pull without any database edits. How do we achieve this?

    Firstly, our wp-config across all live/staging/dev environments should contain the standard multisite settings:

    define( 'WP_ALLOW_MULTISITE', true );
    define( 'MULTISITE', true );
    define( 'SUBDOMAIN_INSTALL', false );
    define( 'DOMAIN_CURRENT_SITE', 'example.com' );
    define( 'PATH_CURRENT_SITE', '/' );
    define( 'SITE_ID_CURRENT_SITE', 1 );
    define( 'BLOG_ID_CURRENT_SITE', 1 );
    

    Notice that DOMAIN_CURRENT_SITE is the live domain! In order for this whole system to work, the domains defined in our database should all be the live domains, and we will handle each staging/dev site with the following, also in wp-config (only on dev/staging configs, not in live config):

    /** Set this to your local tld setup */
    define( 'WP_HOME', 'http://example.local');
    define( 'WP_SITEURL', 'http://example.local');
    /** Include wp-content/sunrise.php */
    define( 'SUNRISE', true );
    /** This should be the TLD in the database */
    define( 'WP_PROD_TLD', '.com' );
    /** This should be the tld of your local copy */
    define( 'WP_DEV_TLD', '.local');
    

    The WP_PROD_TLD and WP_DEV_TLD are our own custom constants that we will use to check if we need to alter the domain (I realize that you are also using subdomains in your setup, but I'd like to describe a slightly more "standard" approach before attempting to fit your specific circumstance). The SUNRISE constant is built into WP multisite which just says to include /wp-content/sunrise.php if it exists. Here is our sunrise.php file:

    <?php
    /**
     * File sunrise.php
     *
     * This allows us to copy the production multisite database to staging/dev and still use 
     * it directly without altering domains
     */
    
    /**
     * Filter /wp-includes/ms-load.php get_site_by_path to find production domains
     **/
    function dev_get_site_by_path($_site, $_domain, $_path, $_segments, $_paths) {
        global $wpdb, $path;
    
        // Get our actual domain in the database (should be set to production domain)
        // The domain coming in should be the request domain
        $domain = str_replace( WP_DEV_TLD, WP_PROD_TLD, $_domain);
    
        // Search for a site matching the domain and first path segment
        $site = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM $wpdb->blogs WHERE domain = %s and path = %s", $domain, $_paths[0] ) );
        $current_path = $_paths[0];
    
        if ($site === null) {
        // Specifically for the main blog - if a site is not found then load the main blog
            $site = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM $wpdb->blogs WHERE domain = %s and path = %s", $domain, '/' ) );
            $current_path = '/';
        }
    
        // Set path to match the first segment
        $path = $current_path;
    
        return $site;
    }
    add_filter('pre_get_site_by_path', 'dev_get_site_by_path', 1, 5);
    add_filter('pre_get_network_by_path', 'dev_get_site_by_path', 1, 5);
    
    /**
     * Filter the site_url and home options for each site, and
     * filter /wp-includes/link-template.php::network_site_url()
     * and /wp-includes/link-template.php::network_home_url()
     * so that our network site link is correct in the admin menu
     */
    function dev_network_url( $_url = '' ) {
        return str_replace( WP_PROD_TLD, WP_DEV_TLD, $_url );
    }
    add_filter( 'network_site_url', 'dev_network_url' );
    add_filter( 'network_home_url', 'dev_network_url' );
    add_filter( 'option_siteurl', 'dev_network_url' );
    add_filter( 'option_home', 'dev_network_url' );
    

    Note that the sunrise.php file is ONLY INCLUDED VIA WP-CONFIG ON DEV/STAGING, never on the live server! This is filtering the request to get a network "site by path", which just means it looks at the domain and path requested from the client, and matches the domain and path in the wp_blogs table. We are checking the domain for our live TLD (in this case, .com) and replacing it with the TLD of the current environment (our local copies are .local, the staging server is .staging.com). It's also filtering the WP_HOME and WP_SITEURL options per site at the bottom.

    And, that's it! The live domains in the database are being translated on the fly for your local/staging site setup! As long as you're using subfolders or you have aliases set up in your local vhost settings, this should work.

    Now, for your case of using subdomains, you might have to do a more thorough filtering of the domains instead of just replacing the TLD. Maybe you settle for the "canonical" domains in the database being multisite.dev and then in your sunrise.php file just append the local user's subdomain instead of replacing the TLD, and then do replacements in the database for the live domains once you're ready to migrate.

    Credit to "laubsterboy" for getting me started towards this solution: https://www.laubsterboy.com/blog/2014/08/running-development-copy-wordpress-multisite-update/