Deploy CakePHP Apps on AppFog the Right Way

This article covers two topics: deploying an application to the AppFog cloud provider, and best practices for a well-structured PHP application.

First we’ll build a simple note-taking application using CakePHP and its automated bake command. Then, I’ll show you one way to structure the application separating its components into:

  • application code
  • application data and settings
  • library and vendor code

The app will finally be registered and published to AppFog.

Sign Up with AppFog

AppFog is a Platform as a Service (PaaS) provider built on top of the open-source Cloud Foundry project. It provides support for multiple programming languages and lets us deploy applications to multiple infrastructure providers, such as Amazon AWS, HP Openstack, Microsoft Azure, and Rackspace DFW.

Signing up with AppFog is very simple; all you need is an email address and a password and you have access to their free plan which offers 2GB of RAM, 10 services (ex. databases) with a max of 100MB each, and a 50GB data transfer limit. That’s lot for a free service! Also, as they say, you can move your app to any Cloud Foundry compatible service.

Go ahead and create your free AppFog account, and then we’ll continue.

Create the Local Application

Download the latest version of CakePHP and extract it on your development machine, and then rename it as your application (I choose FogNotes). Make this directory visible by your web server with a friendly URL (ex. http://fognotes.local), the document root should be the app/webroot directory, and create an empty MySQL database (ex. fognotes_local).

In your browser, you should see the default homepage provided by CakePHP. Don’t worry about the warnings – we’ll take care of them soon enough.

Now create a directory named data and two subdirectories within it named config and logs. Copy (or move) the app/tmp directory inside data and make this and the logs directory writable to the web server. As a finishing touch, create an alias of CakePHP’s console command cake in the root directory of your application.

~/FogNotes$ ln -s lib/Cake/Console/cake cake

Your directory structure should look like this:

FogNotes/
    app/
    data/
        config/
        logs/
        tmp/
            cache/
            logs/
            sessions/
            tests/
    lib/
        Cake/
    plugins/
    vendors/
    cake
    index.php

By doing this, we have separated the specific application code (app) from the framework (lib/Cake) and libraries (plugins and vendors). Also, the application’s data and settings are all in one separate place (data/*). Now we can we can now update, backup, and maintain each of the components separately. This is much desired in a real-world application!

With the directory structure in place, let’s tell CakePHP to use it. We must set the custom temp directory, so edit app/webroot/index.php and paste this code just after the define('ROOT',...) statement:

<?php
// Custom TMP directory
if (!defined('TMP')) {
    define('TMP', ROOT . DS . 'data' . DS . 'tmp' . DS);
}

Now for the database. Normally the database connection settings are stored inside the app/Config directory in a file named database.php. We’ll modify this setting in order to have the framework look in data/config directory for a file whose named after our current environment instead (ex. data/config/local.php).

Copy the file app/Config/database.php.default to app/Config/database.php and replace the DATABASE_CONFIG class with this code:

<?php
class DATABASE_CONFIG
{
    public $default = null;
    public $test    = null;
    public $env     = null;

    /**
     * Switch configuration based on environment
     */
    function __construct() {
        // Check that a suitable environment is defined
        if (!defined('APP_ENV')) {
            return false;
        }

        $this->env = APP_ENV;

        // Try to read database settings from the current ENV file
        $config = Configure::read('Database.config');
        if (!is_array($config)) {
            return false;
        }

        // Load all config database profiles
        foreach ($config as $name => $data) {
            $this->$name = $data;
        }

        // Throw an error if there is no suitable configuration
        if (empty($config['default']) || empty($this->default)) {
            return false;
        }
    }
}

This trick was adapted from an article published in CakePHP developers community by eimermusic. The environment will be defined using the bootstrap file or reading an environment variable provided by AppFog and the associated config file will be loaded.

The app/Config/bootstrap.php file is the startup file for the application. Near the bottom of the file, after the Configure::write('Dispatcher.filters'...) statement, insert this code that registers the data/config path with CakePHP:

<?php
// Define a custom reader to search for config files
App::uses('PhpReader', 'Configure');
Configure::config('default',
    new PhpReader(ROOT . DS . 'data' . DS . 'config' . DS));
// Determine the environment
if ($env = getenv('APP_ENV')) {
    if (!defined('APP_ENV')) define('APP_ENV', $env);
}
else {
    if (!defined('APP_ENV')) define('APP_ENV', 'local');
}

AppFog will let us set the APP_ENV variable. If it’s not set, then the app considers itself in local mode.

Now skip to the end of the file and paste this code:

<?php
try {
    Configure::load(APP_ENV);
}
catch (ConfigureException $e) {
    // do something, for example exit application
}

The Configure::load() statement tells CakePHP to search the data/config directory for a file named like the current environment (exs. local.php or prod.php). Within this file we can override all the settings defined earlier by the application.

A sample local.php config file could be:

<?php
// Load base configuration. Any of the core settings can be
// overridden here. Don't delete the $config var!
$config = array(
    'debug' => 1
);

// If declared in the $config array, String::UUID() crashes
Configure::write('Security.salt', 'SomeSuperSecretLongString');
Configure::write('Security.cipherSeed', 'SomeSuperSecretLongNumber');

// Database settings
Configure::write('Database.config', array(
    'default' => array(
        'datasource' => 'Database/Mysql',
        'persistent' => false,
        'host' => 'localhost',
        'login' => 'root',
        'password' => 'secret_password',
        'database' => 'fognotes_local',
        'prefix' => '',
        'encoding' => 'utf8',
        'unix_socket' => '/tmp/mysql.sock',
    )
));

// Custom Log file settings to ROOT/data/logs/
CakeLog::config('default', array(
    'engine' => 'FileLog',
    'path'   => ROOT . DS . 'data' . DS . 'logs' . DS
));

With all the customization in place, the warning messages in the default home page should disappear.

Create the Application in AppFog

Log into your AppFog console and create a new application. Be sure to choose PHP as the application type, your favorite infrastructure provider (I chose AWS Europe), and give it a name. Then click “Create App” and, in just a couple of seconds, the system will send you to the app’s “Mission Control”.

The application with AppFog initally consists of a single index.php file with “Hello World” code; we have to upload our local code. But before we do, we need to bind a database service. Select the “Services” panel, click on the MySQL button, and give it a name (ex. fognotes_prod). The credentials to connect to the service are stored into the environment variable VCAP_SERVICES.

We also need to go to the “Env Variables” panel and set our custom variable. Create a new variable named APP_ENV with the value of “prod” and save.

Make a copy of data/config/local.php and name it data/config/prod.php and open it with your editor. We’ll extract the database settings following the guidelines provided by the AppFog docs. Edit the configuration file so that it looks like this:

<?php
// ...other code

// AppFog Extraction
$services_json = json_decode(getenv('VCAP_SERVICES'),true);
$af_mysql_config = $services_json['mysql-5.1'][0]['credentials'];

// Database settings
Configure::write('Database.config', array(
    'default' => array(
        'datasource' => 'Database/Mysql',
        'persistent' => false,
        'host' => $af_mysql_config['hostname'],
        'login' => $af_mysql_config['username'],
        'password' => $af_mysql_config['password'],
        'database' => $af_mysql_config['name'],
        'prefix' => '',
        'encoding' => 'utf8',
    )
));

// ...other code

We could also bind the application to a custom domain or subdomain (for free!) but that’s not the case here.

Now we’re almost ready to push our code! In the “Update Source Code” panel, find the instructions to install the af utility on your system. Open a terminal session and navigate into your application root. Follow the instructions to install the CLI tool, then login but don’t download the remote project code. Instead, go ahead with the command af update FogNotes to push the local code to the remote server. In less then a minute, your application is deployed and you can should see the same home screen as your local version. This means the application recognizes the production environment and the remote database.

Adding Some Finishing Touches

We have an empty application deployed, but we must add at least some code and data for our app. We’ll let CakePHP do the hard work.

Create the file data/config/notes.sql with the following content:

CREATE TABLE notes (
    id INT NOT NULL AUTO_INCREMENT PRIMARY KEY ,
    title VARCHAR( 255 ) NOT NULL ,
    content TEXT NOT NULL ,
    created DATETIME NULL DEFAULT NULL ,
    modified DATETIME NULL DEFAULT NULL ,

    INDEX (title, created, modified)
)
ENGINE = INNODB;

Import the file into you local database on the command line or with your favorite MySQL tool.

Next, from your app’s root directory, run the command:

~/FogNotes$ ./cake bake

Follow the instructions (it’s really easy!) and create a model, view, and controller for “Notes”, which is the only table in the database. CakePHP will create the right files for you. Then, open the file app/Config/routes.php and replace the first Router::connect() statement with the following code to re-link the home page to the list of notes:

<?php
Router::connect('/',
  array('controller' => 'notes', 'action' => 'index'));

Reload the local home page and you should have a fully functioning application. Now it’s time to publish the changes. The command af update FogNotes will push the new code.

You’ll see a nasty error page if you reload the remote app now. This is because we haven’t created the data table on the remote database. Woops!

The easiest way to import tables is by creating a tunnel with the af tunnel command and then connect to the remote database using the info provided by the tunnel itself. Open the tunnel in a console window, select “none” as client type, and then you can use another terminal window to import the SQL file:

mysql --protocol=TCP --host=localhost --port=10000 --user=RemoteUserName 
 --password=RemotePassword RemoteLongDifficultDBName < /path/to/notes.sql

You can also use Sequel Pro if you are on a Mac or MySQL Workbench as long as the tunnel is open.

Once the table is in place, the remote application will run smoothly again.

Summary

The organization of the sample application I showed to you here is just one of many techniques you can use to improve your CakePHP development processes. For example, you could store sessions to a file in your local machine and use MongoDB in an AppFog production instance.

AppFog is a relatively young PaaS service, but is promising. It’s fast and easy to use, and it’s always evolving. AppFog also lets you clone an entire application, useful to create a staging or bug fixing environment. The rest is up to you, so happy coding!

Free book: Jump Start HTML5 Basics

Grab a free copy of one our latest ebooks! Packed with hints and tips on HTML5's most powerful new features.

  • Luc Perkins

    You’ve done it again, Vito! Great stuff.

  • Anil Kumar Panigrahi

    Thanks for posted great information ..

  • Josue Del Cid

    Hey.. can you put the whole app into a git repository or a zip file so we can download it?
    Thanks..