Projects with nested WordPress installations

Josh J edited this page Oct 1, 2013 · 5 revisions

Overview

We recently had a few Magento projects that contained one or more WordPress installations for cases where the customer wanted a unique blog per multisite. While this was extracted from a Magento capistrano script there's no reason it couldn't be used for other types projects as well.

This documentations assumes a few things:

  • you already have a working Capistrano deployment script for deploying your main project
  • your WordPress installations are located in separate folders (i.e., project_root/blog1, project_root/blog2). They don't necessarily have to be in the project's root directory but this example will assume so.
  • you have a separate wp-config.php file for each of your environments (i.e., wp-config.staging.php, wp-config.production.php)
  • you are storing your WordPress database tables within the same database as your main project
  • you are naming your WordPress database tables with some sort of prefix (i.e., "wp_") and this is reflected in your wp-config.php files (for every environment)

Define WordPress variables

# Capfile (continued)

# --------------------------------------------
# Setting nested WordPress variable defaults
# --------------------------------------------

# ------
# Database Credentials
# assumes you're using the same database user, password and database
# as your main project would use; if you are not doing so, simply use
# the following template for defining the credentials:
#
#    Alternative Database Credential Configuration:
#      set :wp_db_user, "mycustomuser"
#      set :wp_db_name, "custom_wp_database"
#      set :wp_db_pass, proc{ Capistrano::CLI.password_prompt("Database password for '#{wp_db_user}':") }
# ------
set (:wp_db_user) { "#{dbuser}" }  
set (:wp_db_name) { "#{dbname}" }
set (:wp_db_pass) { "#{dbpass}" }

Tell Capistrano about your WordPress install(s)

# Capfile (continued)

# ------
# Multi-WordPress installations
# Create an array of configuration settings for 
# each of the WordPress sites that should follow
# the following defined for each installation:
#
#    :wp_blogs       # array, contains hashes with each hash 
#                    #   containing options for a given WordPress 
#                    #   installation
#
#    :directory      # string, relative file path to the WordPress 
#                    #   installation from the project's root directory
#
#    :db_prefix      # string, table prefix for all WordPress tables
#
#    :base_url       # hash, contains key/value pairs for environments and 
#                    #   their full URLs 
#                    #   NOTE: DO NOT INCLUDE the trailing slash "/" in the URL
#
# The following is an example of a well formed and 
# valid configuration array for multiple WordPress
# installations
#     
#    set :wp_blogs, [
#       { :directory => "blog1", :db_prefix => "wp1_",
#         :base_url => {
#           :staging => "http://staging.example.com",
#           :production => "http://www.example.com"
#         }
#       },
#       { :directory => "blog2", :db_prefix => "wp2_",
#         :base_url => {
#           :staging => "http://staging.anotherexample.com",
#           :production => "http://www.anotherexample.com"
#         }
#       }
#     ]
# ------
set :wp_blogs, [
  { :directory => "blog1", :db_prefix => "wp1_",
    :base_url => {
      :staging => "http://staging.example.com",
      :production => "http://www.example.com"
    }
  },
  { :directory => "blog2", :db_prefix => "wp2_",
    :base_url => {
      :staging => "http://staging.anotherexample.com",
      :production => "http://www.anotherexample.com"
    }
  }
]

Almost there! Now create some WordPress tasks automate the process

# Capfile (continued)

namespace :wordpress do
  desc "Setup nested WordPress install"
  task :setup, :roles => :web do
    wordpress.setup_shared
  end

  desc "Setup shared folders for WordPress"
  task :setup_shared, :roles => :web do
    wp_blogs.each do |blog|
      wp_blog_directory = blog[:directory]
      
      # create shared directories
      run "mkdir -p #{shared_path}/#{wp_blog_directory}/uploads"
      run "mkdir -p #{shared_path}/#{wp_blog_directory}/cache"
      # set correct permissions
      run "chmod -R 755 #{shared_path}/#{wp_blog_directory}"
    end
  end

  desc "[internal] Removes unnecessary files and directories"
  task :prepare_for_symlink, :roles => :web, :except => { :no_release => true } do
    wp_blogs.each do |blog|
      wp_blog_directory = blog[:directory]
      wp_uploads_path   = "#{wp_blog_directory}/wp-content/uploads"
      wp_cache_path     = "#{wp_blog_directory}/wp-content/cache"

      # remove shared directories
      run "rm -Rf #{latest_release}/#{wp_uploads_path}"
      run "rm -Rf #{latest_release}/#{wp_cache_path}"

      # Removing cruft files.
      run "rm -Rf #{latest_release}/#{wp_blog_directory}/license.txt"
      run "rm -Rf #{latest_release}/#{wp_blog_directory}/readme.html"
    end
  end

  desc "Links the correct settings file"
  task :symlink, :roles => :web, :except => { :no_release => true } do
    # internal call to wordpress:setup to get it out of the callback stack
    wordpress.setup
    
    # internal call to the :prepare_for_symlink task
    wordpress.prepare_for_symlink

    # symlink files/directories
    wp_blogs.each do |blog|
      wp_blog_directory = blog[:directory]
      wp_uploads_path   = "#{wp_blog_directory}/wp-content/uploads"
      wp_cache_path     = "#{wp_blog_directory}/wp-content/cache"

      run "ln -nfs #{shared_path}/#{wp_blog_directory}/uploads #{latest_release}/#{wp_uploads_path}"
      run "ln -nfs #{shared_path}/#{wp_blog_directory}/cache #{latest_release}/#{wp_cache_path}"
      run "ln -nfs #{latest_release}/#{wp_blog_directory}/wp-config.#{stage}.php #{latest_release}/#{wp_blog_directory}/wp-config.php"
    end

    # protect the config file! (internal call to wordpress:protect method)
    wordpress.protect
  end
  
  desc "Set WordPress Base URL in database"
  task :updatedb, :roles => :db, :except => { :no_release => true } do
    wp_blogs.each do |blog|
      wp_blog_directory   = blog[:directory]
      wp_db_prefix        = blog[:db_prefix]
      wp_base_url_prefix  = blog[:base_url]["#{stage}".to_sym]
      wp_base_url         = "#{wp_base_url_prefix}/#{wp_blog_directory}"

      run "mysql -u #{wp_db_user} --password='#{wp_db_pass}' -e 'UPDATE #{wp_db_name}.#{wp_db_prefix}options SET option_value = \\"#{wp_base_url}\\" WHERE option_name = \\"siteurl\\" OR option_name = \\"home\\"'"
    end
  end
  
  desc "Protect system files"
  task :protect, :except => { :no_release => true } do
    wp_blogs.each do |blog|
      wp_blog_directory = blog[:directory]
      run "chmod 444 #{latest_release}/#{wp_blog_directory}/wp-config.php*"
    end
  end
end

NOTE

The coding syntax for the wordpress:updatedb method above doesn't seem to be able to handle escaped characters so the run mysql... command should be:

run "mysql -u #{wp_db_user} --password='#{wp_db_pass}' -e 'UPDATE #{wp_db_name}.#{wp_db_prefix}options SET option_value = \\"#{wp_base_url}\\" WHERE option_name = \\"siteurl\\" OR option_name = \\"home\\"'"

If you see double backslashes (\\) in the command you copied, you will need to remove the extra backslash (\).

Last Step! Add the WordPress tasks to your task chain

Although we're using Magento in this example, the wordpress:symlink task could be called after project-specific tasks have completed or at least right before the deploy process is finished (as long as the latest_release variable is using your latest deployment as it's value you should be OK).

# Capfile (continued)

# --------------------------------------------
# Task chains
# --------------------------------------------
before "deploy:update_code", "backup"
after "deploy:symlink", "magento:symlink"
after "magento:symlink", "wordpress:symlink"
after "wordpress:symlink", "wordpress:updatedb"