I have been doing some dusting on projects and workflows and while at it, easy deployment of changes is a recurrent issue. Simply synchronizing files between the local development environment and the production server is generally not a problem, most IDEs already offer easy SFTP integration etc. However, at least on projects up to a certain scale (blogs, small websites, etc), you will often want to also synchronize website data, such as the database and file uploads.

Hooking Continuous Integration platforms such as DeployBot or Gitlab to git events would probably achieve this, but there seems to be too much overhead when setting up a new project, as well as the introduction of dependency over a specific platform. The idea of having a portable solution, based off simple script templates seems more manageable to me, in smaller, single-developer projects.

Writing bash scripts is not very fun and there are plenty of task managers available (Mina, Rocketeer) that make it much easier. Yet, when it comes to a traditional PHP stack, for some reason they all seem framework-specific or overly opinionated. After some more research I settled with Robo, which does exactly what I require.

Robo sources its tasks from the Robofile in the current directory. The Robofile is a simple PHP class. Now the advantage of Robo is that you have both access to PHP functions as to Robo built-in tasks (I/O, SSH, rsync...). That means we can access PHP application logic or configuration files within our deployment or maintenance routines, as well as running system commands (the navigation of Robo docs could use some love though).

In this sense I thought it could be interesting to define tasks in a way that makes them useful both in development and production environments. To try it out I coupled it with a Processwire project where the goal is to deploy all files and sync databases.

Deploying Processwire

Processwire is one of my go-to CMS choices for a number of reasons I won't get into here, but if you don't know it already you should check it out. Below I will quickly demonstrate how one can set up Robo to deploy Processwire.

Assuming you currently have a complete copy of your project running locally, you should have two directories at your project root: /site and /wire. If you're using git, I also assume you're tracking at the root level. On the remote end, you are only required to have a ready, valid /site/config.php. For simplicity sake, I also have public keys set up to SSH the remote server. 

The first thing you will need is to install Robo, let's use composer:

cd site && composer init --require="codegyre/robo" -n

Now, let's create our Robofile at /site/Robofile:

<?php

// PROCESSWIRE is required for the config file the be read.
define('PROCESSWIRE', true);

class RoboFile extends \Robo\Tasks {

    /**
     * @var string $host            Remote host.
     */
    var $host = "host.com";
    /**
     * @var string $ssh_user        User on remote server, used for SSH login.
     */
    var $ssh_user = "user";
    /**
     * @var string $remote_path     Path on remote server.
     */
    var $remote_path = "/home/user/web/host.com/public_html";

    /**
     *  Require Processwire config so we can read database details.
     */
    function __construct() {
        global $config;
        $config = new stdClass();
        if (file_exists('./config.php')) {
            require './config.php';
        } else {
            echo "config.php was not found.\n";
            die();
        }
    }

    /**
     * Pushes changes to remote server. By default will also sync database. Use false to disable remote database import.
     *
     * @param bool|true $remote_import       Sync database?
     */
    function remotePush($remote_import = true) {
        $this->dbDump();
        $this->filesSyncToRemote();
        if ($remote_import && $remote_import !== 'false')
            $this->filesSyncToRemotePost();
    }


    /**
     * Pulls changes from remote server. Will also sync database.
     */
    function remotePull() {
        $this->taskSshExec($this->host, $this->ssh_user)
            ->remoteDir($this->remote_path . '/site')
            ->exec('composer install')
            ->exec('php vendor/bin/robo db:dump')
            ->run();
        $this->filesSyncFromRemote();
        $this->dbImport();
    }

    /**
     * Creates local database dump.
     */
    function dbDump() {
        global $config;
        $this->_exec("mkdir -p ./database");
        $this->_exec("mysqldump -u $config->dbUser -p$config->dbPass $config->dbName > ./database/database.sql");
    }


    /**
     * Locally imports database dump, if it exists.
     */
    function dbImport() {
        global $config;
        if (file_exists('database/database.sql')) {
            $this->_exec("mysql -u $config->dbUser -p$config->dbPass $config->dbName < ./database/database.sql");
        } else {
            echo "No database file found.";
        }
    }

    /**
     * Push local files to remote server.
     *
     * Will ignore files in .gitignore.
     * Will include files in .rsync-include
     */
    function filesSyncToRemote() {
        $this->taskRsync()
            ->fromPath('../')
            ->toHost($this->host)
            ->toUser($this->ssh_user)
            ->toPath($this->remote_path)
            ->delete()
            ->recursive()
            ->excludeFrom('../.gitignore')
            ->excludeVcs()
            ->checksum()
            ->wholeFile()
            ->progress()
            ->humanReadable()
            ->stats()
            ->run();

        // push extra files
        $this->taskRsync()
            ->fromPath('../')
            ->toHost($this->host)
            ->toUser($this->ssh_user)
            ->toPath($this->remote_path)
            ->recursive()
            ->delete()
            ->excludeVcs()
            ->checksum()
            ->wholeFile()
            ->progress()
            ->humanReadable()
            ->stats()
            ->filesFrom('../.rsync-include')
            ->run();
    }

    /**
     * Make remote server import database dump.
     */
    function filesSyncToRemotePost() {
        $this->taskSshExec($this->host, $this->ssh_user)
            ->remoteDir($this->remote_path . '/site')
            ->exec('composer install')
            ->exec('php vendor/bin/robo db:import')
            ->run();
    }

    /**
     * Pull files from remote.
     */
    function filesSyncFromRemote() {
        $this->taskRsync()
            ->fromHost($this->host)
            ->fromUser($this->ssh_user)
            ->fromPath($this->remote_path . '/')
            ->toPath('./')
            ->recursive()
            ->excludeFrom('../.gitignore')
            ->excludeVcs()
            ->checksum()
            ->wholeFile()
            ->progress()
            ->humanReadable()
            ->stats()
            ->run();
    }

}

By executing php vendor/bin/robo list you should now get a list of the available tasks.

db
    db:dump                    Creates local database dump.
    db:import                  Locally imports database dump, if it exists.

files
    files:sync-from-remote     Pull files from remote.
    files:sync-to-remote       Push local files to remote server.
    files:sync-to-remote-post  Make remote server import database dump.

remote
    remote:pull                Pulls changes from remote server. Will also sync database.
    remote:push                Pushes changes to remote server. 
                               By default will also sync database. 
                               Use false to disable remote database import.

A couple of things to note about those:

  • Robo allows you to specify SSH user password instead of using public keys.
  • /.gitignore is also used by rsync to exclude files.
  • to circumvent that, you can list files to be included at /.rsync-include, one pattern per line. This is useful if you don't commit your minified scripts and styles but still want to push them to production.
  • database export/import requires /site/config.php to be properly set.

A this stage, make sure you have the remote /site/config.php ready, and execute php vendor/bin/robo remote:push. A database dump will be created locally, files will be synced to the remote host and a remote database import is initiated.

Now although we could add some more functions such as hooking it to git operations and whatnot, again, portability goes a long way and this Robofile is a nice template to have. Hence, I've created a repository with the starter pack, including composer integration.

 

Published by António Andrade on Sunday, 10th of April 2016. Last updated on Thursday, 31st of May 2018. Opinions and views expressed in this article are my own and do not reflect those of my employers or their clients.

Cover image by Tom Eversley.

Are you looking for help with a related project? Get in touch.

Replies

  • Alex

    Posted by Alex on 11/05/16 6:12pm

    Thanks for posting this! Absolutely amazing.

  • olidev

    Posted by olidev on 12/27/17 9:01am

    I find using Envoyer quite easy because it gives you a UI. No need to use any command line for deploying from git repo (here is an example: https://www.cloudways.com/blog/php-laravel-envoyer-deployment/ ). Even though Envoyer is Laravel product, it can be used with any PHP app regardless of the CMS or framework it is using.

  • Sanne

    Posted by Sanne on 03/09/19 9:45pm

    Hello, This is somehting I have looking for some time.
    I used your examples to include this to a PW site.
    I get errors on rsync and mysql.
    Could this be due to the fact I am on windows?
    I use wamp for local dev.

Post Reply