Deploying a CakePHP app with Capistrano

Deploying a CakePHP app with Capistrano

During my internship i was working on a large project and at the end of the process it became more and more difficult to keep the development version and the live version in sync while maintaining different configurations.

A couple of colleagues used Capistrano for their Rails projects and i tried to modify Capistrano so that it would work with CakePHP.

Password-less login

First we need to be able to login without providing a password this guide from Brian Rosner describes the steps to do this.
The reason for this password-less login is that you don’t have to provide a password when you deploy this way.

Capistrano

Next we need to get Capistrano. I work on a mac with OSX 10.5, witch has Capistrano built-in. If you have never updated this version of capistrano you might want to gem update your version.

Folder structure

capistrano folder structure
The image above shows my folder structure. The current folder is actually a symlink to the latest deploy of my project. The logs is for my apache access and error logs and the releases folder contains the latest 10 releases deployed to the server.

The shared folder contains files and folders that i want to use instead the ones i deploy. In my case i keep the /app/config folder here.

Capfile

In the root of my project folder i created a capfile (no extension)

that contains the following code:

load 'deploy' if respond_to?(:namespace) # cap2 differentiator
role :web, 'example.com'
ssh_options[:username] = 'notrootofcourse'
ssh_options[:forward_agent] = true

set :scm, :git
set :scm_verbose, true
set :repository, 'git@git.example.com:repository'
set :branch, 'master'
set :deploy_via, :remote_cache
set :use_sudo, false
set :application, 'application_name'
set :deploy_to, "/data/webservers/#{application}/"

namespace :deploy do
  task :start do
  end
  
  task :stop do
  end
  
  task :restart do
  end

  desc <<-DESC
    Symlinks shared configuration and directories into the latest release
    Also clear persistent and model cache
  DESC
  task :finalize_update do
    run "rm -rf #{latest_release}/app/config; ln -s #{shared_path}/app/config #{latest_release}/app/config"
    run "rm -rf #{latest_release}/app/tmp/models/*"
    run "rm -rf #{latest_release}/app/tmp/persistent/*"
  end
  
  
end

namespace :tail do
  task :default do
    run "tail -f #{deploy_to}/logs/*.log"
  end
end

The first lines of the Capfile set some global configuration options, like your ssh username that can login without providing a password, the (in my case git) repository to pull the latest release from and the name and path of the application.

To make capistrano work with (Cake)PHP you need to overwrite all the default tasks that capistrano runs when deploying. Since i use apache and php, we don’t need to restart the webserver, so we just create a :restart task that does nothing, the same goes for :start and :stop

in the :finalize_update task i link the /config dir of my CakePHP app to the shared folder i described above. This way i always use the live configurations instead of my dev configs. Also, just to be sure, i remove the cached data.

All there is to do is a “cap deploy” in the root folder of your project and capistrano will deploy the latest version to your server.

This is of course just a simple example of a capistrano config and you can create your own folder structure and run your own commands.

Besides deploying i also created a :tail namespace, witch tails my log files. This way i can do a “cap tail” to see what’s going on with apache without logging in trough ssh. Other possible functions could be a “cap clearcache” to remove all the cache files.

Filed Under: CakePHP, English - read on

Permanent appended named parameters in routes with CakePHP

For a project i’m working on i needed a bit of custom routing.
Basically the project is a shop where you can edit multiple events and i stored the current event in a session.
But the request came if the client could edit multiple events at the same time.
Long story short, i needed the event_id somewhere in the url and this is where the router comes in to play.

There are a couple of ways to get the event_id in the url with custom routing.

Option one

Use a named parameter

this method will append event_id:[UUID] to the url.
for example: http://www.example.com/products/index/event_id:490ed5ab-ed6c-4034-8205-0264d5453078

in /app/config/routes.php

Router::connectNamed(array('event_id'=>$UUID), array('default' => true, 'greedy' => false));

This will let the router know that we have a named part called event_id.
It will also check if event_id is an UUID (other possibilities are $ID, $day, $month and $year, or a regular expression).

Helper

This was the easy part, because you need to tell all the helpers that create an url to use ‘event_id’=>’[UUID]‘.
In my case i already wrote an app and i didn’t feel like rewriting all links and forms, so i created a file called app_helper.php in the app folder like so:

/app/app_helper.php

<?php
class AppHelper extends Helper { 
        function url($url = null, $full = false) { 
                if (is_array($url)) { 
                        if (empty($url['event_id']) && isset($this->params['named]['event_id'])) { 
                                $url['event_id'] = $this->params['named]['event_id']; 
                        }
                } 
                return parent::url($url, $full); 
        } 
} 
?>

What this does is append the event_id to all url arrays and then calls the parent url function (in /cake/lib/helper/helper.php).
Because all helpers extend AppHelper, all url calls will go tough the above function.

Redirect

Now we only need to address one more issue, namely the $this->redirect(); function in the controllers, because we want the event_id appended to those url’s too.

/app/app_controller.php

function redirect($url, $status = null, $exit = true) { 
 	if (is_array($url)) { 
        if (empty($url['event_id']) && isset($this->params['named]['event_id'])) { 
        	$url['event_id'] = $this->params['named']['event_id']; 
        }
 	}
    return parent::redirect($url, $status, $exit); 
}

All controllers extend app_controller so they will all use this new redirect function.

So once you add event_id:[UUID] to an url, it will always be appended.
the event id can be retrieved in all controllers by calling $this->params['named']['event_id'] or $this->passedArgs['event_id']

Option two

The other way to do this is by changing a lot of routes in the routes.php file, but the url will be prettier (like: http://www.example.com/event_id/controller/action/params)

(/app/config/routes.php)

/*
	 * The following section is to add the event_id to each backend url
	 * The event_id is added in the url method of app_helper.php and redirect method of app_model.php
	 */
	Router::connect('/:event_id/:controller', 
			array('controller'=>':controller'), array('event_id'=>$UUID));

	Router::connect('/:event_id/:controller/:action', 
			array('controller'=>':controller', 'action'=>':action'), array('event_id'=>$UUID));

	Router::connect('/:event_id/:controller/:action/:id', 
			array('controller'=>':controller', 'action'=>':action', 'id'=>':id'), array('pass'=>array('id'),'event_id'=>$UUID));

	Router::connect('/:event_id/:controller/:action/:id/*', 
			array('controller'=>':controller', 'action'=>':action', 'id'=>':id'), array('pass'=>array('id'), 'event_id'=>$UUID));

	Router::connect('/:event_id/:controller/:action/*', 
			array('controller'=>':controller', 'action'=>':action'), array('event_id'=>$UUID));

I’m not shure if this is the most efficient way to cover all routes, but if you replace the code from the first block with this code, you get the nice url (http://www.example.com/event_id/controller/action/params).

The last array makes shure only uuid’s are used by the router.

Filed Under: News - read on

Serializeable behavior for CakePHP

For a project i’m working on i needed to serialize a number of arrays before they could be saved and since i don’t like to serialize and un-serialize all those arrays in each controller action, i wrote a behavior to take care of this.

It serializes all array data before it is saved into the database and when you do a find it will unserialize all data(including related model data).

Download serializeable behavior

Filed Under: CakePHP, Code, English - read on

Easy forms with CakePHP

Last night i was thinking about the fact that in some cases i can’t use bake to re-bake my views to contain a new field for example. CakePHP also wants you to not write the same code twice, but what about the views. Every time you add a field to your model, you either need to re-bake your views, or update both the edit and add template files. You could use $form->inputs(); but the helper creates form fields for database fields you probably want to keep hidden (_count fields for example).

I can think of two solutions for this problem, namely.

1. Go the partial way (like rails uses it). Basically you make an element with all the form fields you want to display and use the $this->renderElement in the view to display the form fields.

2. Define public fields in your controller and use those. An example below:

post has the following fields:
id, title, content, created, comments_count.

we only want the user to be able to edit the title, content and maybe the created fields.

in our controller we create a new var: “publicFields”


var $publicFields = array('title', 'content', 'created');

you can set this var in the beforeRender callback like this: (controller)


function beforeRender(){
$this->set('publicFields', $this->publicFields);

and in your view you render the inputs using the inputs function of the form controller like:


$form->inputs($publicFields);

this way you only have to add a new field in your database and in the $publicFields array and all your forms update automagically.

What do you use to reduce the amount of code you need to edit for a change in the database?

Filed Under: CakePHP, Code, English - read on

Cakephp routing: prefixes and forms

Prefixes are a nice way to define routes for groups of actions, (see the cookbook).

All admin_ actions go with the /admin// url
For my project i needed both an admin route and a dashboard route, so i made prefixes for them like:

(/app/config/routes.php)

Router::connect('/dashboard/:controller/:action/*', array('prefix' => 'dashboard'));

Now in the view you can use the following:

echo $html->link('Profile', array('controller'=>'users', 'action'=>'dashboard_profile'), array('title'=>'Edit profile'))

But when it comes to forms things start to break.

For example the following:

echo $form->create('User', array('action'=>'dashboard_profile'));

or this one:

echo $form->create('User', array('url'=>array('controller'=>'users', 'action'=>'dashboard_edit')));

generate the following (wrong) link: /users/dashboard_profile/1

While it does point to the right action (dashboard_profile) you get an error message explaining that this is a private action. The only way to use this private action is to use the prefix method.

The reason for this is that the form generate part in the cake code doesn’t do anything with the routing, thus ignoring the prefix we’ve set.

After some digging in the api i found a cake-way to generate the right url:

echo $form->create('User', array('url'=>$html->url(array('action'=>'dashboard_profile'))));

We use the html helper, that does parse the prefix part and generates a nice correct url, namely: /dashboard/user/profile

The html helper does check for any prefixes in the routing and generates the right url.
Combining the form->create and the html->url we get the right url.

Filed Under: CakePHP, Code, English - read on