These are some notes from my deploy of an application I am developing to Heroku. There are some specialized things you need to do to make everything work, so hopefully I catch everything.

Use FriendsOfCake/app-template

The biggest bit here is to ensure that we are properly using composer for everything but our application logic. Most people tend to bundle the CakePHP core with their app in version control, but we can safely rely on Composer to be run before the application is deployed. Having CakePHP installed via composer allows us to safely and quickly test upgrades from one release to another.

composer -sdev create-project friendsofcake/app-template your_app

Add a root index.php

We’ll need it to make the CakePHP app compatible with the buildpack we’ll be using:

<?php
define('APP_DIR', 'app');
define('DS', DIRECTORY_SEPARATOR);
define('ROOT', dirname(__FILE__));
define('WEBROOT_DIR', 'webroot');
define('WWW_ROOT', ROOT . DS . APP_DIR . DS . WEBROOT_DIR . DS);
require APP_DIR . DS . WEBROOT_DIR . DS . 'index.php';
?>

Use environment variables for Configuration

We’ll be using Postgres in production - a big change for many CakePHP developers - because it’s what much of the heroku tooling works around. However, we still need to connect to the database, so here is what I have in my application’s database.php:

<?php
class DATABASE_CONFIG {
  public $default;
  public $test = array(
    'persistent' => false,
    'host' => '',
    'login' => '',
    'password' => '',
    'database' => 'cakephp_test',
    'prefix' => ''
  );
  public function __construct() {
    $DATABASE_URL = parse_url(getenv('DATABASE_URL'));
    $this->default = array(
      'datasource' => 'Database/Postgres',
      'persistent' => false,
      'host'       => $DATABASE_URL['host'],
      'login'      => $DATABASE_URL['user'],
      'password'   => $DATABASE_URL['pass'],
      'database'   => substr($DATABASE_URL['path'], 1),
      'prefix'     => '',
      'encoding'   => 'utf8',
    );
  }
}
?>

This does mean you’ll need to do extra work to get the app running locally, but it shouldn’t be too difficult.

Use CHH/heroku-buildpack-php

This buildpack does a lot of the gruntwork to get a PHP app running to current community standards. Built-in support for Composer, PHP 5.5, PHP-FPM and Nginx. I approve.

heroku config:set BUILDPACK_URL=https://github.com/CHH/heroku-buildpack-php

Configure a CakePHP app in your composer.json

The CHH/heroku-buildpack-php uses our composer.json to figure out how to serve the application. I add an extra key to ensure my app is properly routed.

"extra": {
  "heroku": {
    "document-root": "app/webroot",
    "index-document": "index.php"
  }
}

Use Redis or Memcached for Caching

Both of these are available in the buildpack we use. Distributed caching is much nicer, especially if your dyno can go to sleep. Here is what I use to parse the DSN:

<?php
$login = null;
$password = null;
$server = null;
$servers = null;
if (extension_loaded('apc') && function_exists('apc_dec') && (php_sapi_name() !== 'cli')) {
  $engine = 'Apc';
}
if (getenv('MEMCACHED_URL')) {
  // Custom Memcached implementation
  include ROOT . DS . APP_DIR . DS . 'Lib' . DS . 'Memcached.php';
  $engine = 'Memcached';
  $MEMCACHED_URL = parse_url(getenv('MEMCACHED_URL'));
  $servers = Hash::get($MEMCACHED_URL, 'host');
  $port = Hash::get($MEMCACHED_URL, 'port');
  $login = Hash::get($MEMCACHED_URL, 'user');
  $password = Hash::get($MEMCACHED_URL, 'pass');
} elseif (getenv('REDIS_URL')) {
  // Custom Redis implementation
  include ROOT . DS . APP_DIR . DS . 'Lib' . DS . 'Redis.php';
  $engine = 'Redis';
  $REDIS_URL = parse_url(getenv('REDIS_URL'));
  $server = Hash::get($REDIS_URL, 'host');
  $port = Hash::get($REDIS_URL, 'port');
  $login = Hash::get($REDIS_URL, 'user');
  $password = Hash::get($REDIS_URL, 'pass');
}
$prefix = 'app_';
// In development mode, caches should expire quickly.
$duration = '+999 days';
if (Configure::read('debug') > 0) {
  $duration = '+10 seconds';
}
// Setup a 'default' cache configuration for use in the application.
Cache::config('default', array(
  'engine' => $engine,
  'prefix' => $prefix . 'default_',
  'path' => CACHE . 'persistent' . DS,
  'serialize' => ($engine === 'File'),
  'duration' => $duration,
  'login' => $login,
  'password' => $password,
  'server' => $server,
  'servers' => $servers,
));
?>

Log to a custom path

Your application will not be able to stream logs to you unless you use a custom logging path. Here is how I configured it in my bootstrap.php:

<?php
CakeLog::config('default', array(
    'engine' => 'FileLog',
    'file' => 'stdout.log',
    'path' =>  getenv('LOG_PATH'),
    'types' => array('notice', 'info', 'debug'),
));
CakeLog::config('error', array(
    'engine' => 'FileLog',
    'file' => 'error.log',
    'path' =>  getenv('LOG_PATH'),
    'types' => array('emergency', 'alert', 'critical', 'error', 'warning'),
));
?>

And the configuration:

heroku config:set LOG_PATH=/app/vendor/php/var/log/

Use UTC Date Time

If you’re building a new application, do it correctly. In your core.php, uncomment the datetime call:

date_default_timezone_set('UTC');

Copy plugin assets into the webroot

Because of our virtualhost configuration, plugins will not have their assets served up properly. Here is what I have in my composer.json (under the extra key):

"extra": {
  "heroku": {
    "document-root": "app/webroot",
    "index-document": "index.php",
    "compile": [
      "echo 'Copying DebugKit webroot directory' && cp -rfp $BUILD_DIR/Plugin/DebugKit/webroot $BUILD_DIR/app/webroot/debug_kit"
    ]
  }
}