In this post we will show you how to implement a system where you can have a separate URL for managing, staging and testing a website that is separated from the live/production URL. This system allows you to make changes to a website and preview them before actually going live. It can be implemented with any website, we will show the necessary requirements and steps to implement with symfony and Apostrophe CMS, but you can also take parts of the tutorial and implement them without these applications. We have found this workflow suitable in a number of projects and wanted to share it.

The basic idea is to have an environment where you can make changes, break things, fix them, view them, and when everything is done and revised, “push” them to the live/production environment. The pushing may include database and files (images, css, js) changes. Apostrophe CMS includes the sfSyncContentPlugin, which we already talked about, so we use it to copy the “data” portion of the symfony project from the staging to the production installation. Note that these installations may reside on different directories of the same server or it can perfectly work with different physical servers, having the staging/QA server and a production one.

Instead of having the web server process take care of this, we prefer having a “background” process that runs in the server and does the job. For this requirement we will use Gearman, which allows you to execute commands in the backgroud triggered by a remote process.

Since the website for this project is built using Apostrophe CMS which features a very nice administration UI, we included a “Publish” button on its navigation accessible only to a subset of the CMS users which have the privilege to publish content changes. The Publish button will in effect call a symfony action which in turn triggers the Gearman process in the background, which actually performs the final action.

The whole system is comprised of different applications and components which all play together to create this powerful system, let’s list them here:

We assume that you have Apache and PHP working. Also, for the symfony and Apostrophe CMS sections, we assume you are knowledgeable about this and you have them working.

First, we need to install Gearman and Supervisord. Refer to each software for specifics on how to install these.

With Gearman, we will have a daemon in the background waiting to receive jobs. Supervisord is used to make sure our Gearman daemon keeps running at all times.

Matthew Weier O’Phinney wrote a nice introduction about using Gearman and PHP together.

As for supervisord, installing it is quite easy. Sean Coates has also written a very nice post about the topic.

Our supervisord configuration looks something like this:

[program:gearman]
command=/usr/local/sbin/gearmand -u root
numprocs=1
directory=/var/www/
stdout_logfile=/var/log/supervisord.log
autostart=true
autorestart=true
user=root
[program:gearmanworker]
command=/usr/bin/php /var/www/mysite/batch/gearmanWorker.php
numprocs=1
directory=/var/www/
stdout_logfile=/var/log/supervisord.log
autostart=true
autorestart=true
user=root

This will run a gearmand process and a PHP process with gearmanWorker.php which will receive the tasks and execute them.

We also need to have the gearman PHP extension, installation in most cases is just a matter of executing pecl install gearman and adding extension=”gearman.so” to the php.ini file.

As we said, gearmanWorker.php receives the tasks and execute and commands that are needed to “publish” our website. Here is the code for gearmanWorker.php

function publish($job) {
  $output = '';
  ob_start();
  try {
    $st = time();
    echo "Received job at " .  date("Y-m-d H:i:s", $st) . ": " . $job->handle() . "\n";
    $workload = $job->workload();
    $cmds = unserialize($workload);
    foreach ($cmds as $cmd) {
      echo "\n\n" . date('Y-m-d H:i:s') . " - Executing $cmd\n";
      passthru($cmd);
    }
    $et = time();
    echo "Finished job at " . date("Y-m-d H:i:s", $et) . ": " . $job->handle() . "\n";
    echo "Job time: ".($et-$st)." secs.\n";
    $output = ob_get_clean();
    echo $output;
  } catch (Exception $ex) {
    $output .= "\nException: " . $ex->getMessage();
  }

  mail('my@example.com', 'gearmanWorker job report', $output);
  return true;
}

echo "Starting at " . date("Y-m-d H:i:s") . "\n";

# Create our worker object.
$gmworker = new GearmanWorker();

# Add default server (localhost).
$gmworker->addServer();

# Register function "reverse" with the server. Change the worker function to
# "reverse_fn_fast" for a faster worker with no output.
$gmworker->addFunction("publish", "publish");

print "Waiting for job...\n";
while ($gmworker->work()) {
  if ($gmworker->returnCode() != GEARMAN_SUCCESS) {
    echo "return_code: " . $gmworker->returnCode() . "\n";
    break;
  }
}

The publish symfony action that will initiate the gearman tasks. Create a module/action for this and add the following action code:

public function executePublish(sfWebRequest $request)
{
    $user = $this->getUser();
    if (!$user->hasCredential('cms_admin')) {
      $this->error = 'Unauthorized access.';
      return sfView::ERROR;
    }

    $cfg = sfContext::getInstance()->getConfiguration();

    $env = $cfg->getEnvironment();
    if ($env != "staging") {
      $this->error = 'Wrong environment. Please contact support.';
      return sfView::ERROR;
    }

    $cmds = array();

    $cmds[] = "/usr/bin/php /var/www/mysite/symfony project:disable prod";

    $cmds[] = "/usr/bin/php /var/www/mysite_staging/symfony project:sync-content --sshargs=\"-q\" frontend staging to prod@prod";

    $cmds[] = "/usr/bin/php /var/www/mysite/symfony project:enable prod";

    $gmclient= new GearmanClient();

	// add the default server (localhost)
    $gmclient->addServer();

	// run reverse client in the background
    $job_handle = $gmclient->doBackground("publish", serialize($cmds));

    $output[] = "Finished!";

    $this->output = implode("
", $output); }

Make sure you add a route pointing to the action, something like this:

apps/frontend/config/routing.yml:

publish:
  url:  /publish
  param: { module: mymodule, action: publish }

In he action we check that the environment matches “staging” so we do not run the publish code in the production environment. Then we create a list of commands that will get executed by the gearmanWorker.php which runs as root, or any other user you configure it as, so it has permissions to copy data, clear the cache, etc. In the commands, we execute the cli actions provided by sfSyncContentPlugin which transfer database content and “data” files from one environment to the other.

Finally, we need to add the Publish button to Apostrophe’s Admin Menu. The manual does a great job of explaining how to do so, we need to do it the programmatically way to just show the button when the user has enough privileges, here is a summary:

apps/frontend/config/frontendConfiguration.class.php

class frontendConfiguration extends sfApplicationConfiguration
{
  public function configure()
  {
      $this->dispatcher->connect('a.getGlobalButtons',
      array('Tools', 'getGlobalButtons'));

  }
}

class Tools 
{
  static public function getGlobalButtons()
  {
    $user = sfContext::getInstance()->getUser();
    if ($user->hasCredential('cms_admin'))
    {
      aTools::addGlobalButtons(array(
              new aGlobalButton('publish', 'Publish', '@publish', 'a-publish')));
    }
  }
}

This will add a Publish button to the Admin Menu only when the user has “cms_admin” credential enabled.

When clicking on it, it will call the publish action which in turn creates the list of commands to execute sfSyncContentPlugin finally executed by the gearmanWorker.php

As we said, this post shows how to do it the symfony / Apostrophe way, but if you think with an open mind, this same process can be applied to other softwares like WordPress