Archive for the ‘Zend Framework’ Category

Restful Controllers with Zend Framework

Thursday, August 20th, 2009

I just read Bradley Holt’s post on using HTML 5 forms with REST and HTTP methods and he mentioned the REST router in the recently released version 1.9 of the Zend Framework. The good news is we don’t have to wait for mainstream support for HTML 5.
Zend Framework REST controller supports PUT and DELETE HTTP methods right now through overloading the POST method (ok, it’s a bit of hack – but it’s worked in Rails for ages). Although the ZF 1.9 announcement mentions using Zend_Rest_Route for public APIs, building any application (or parts of it) restfully has a couple of advantages:

  • Consistency and Convention
    REST implies that the application is broken up into resources and these resources are manipulated using the HTTP interface methods (GET, POST, PUT and DELETE). If each controller corresponds to a resource, the controller methods stay consistent throughout the app and that’s a good thing.
  • Clarity
    Using the restful actions force a limit on the number of controller methods preventing the controllers from becoming bloated. This makes them both easier to read and debug.
  • Safety
    A typical restful controller has just two url endpoints eg. a books controller typically has ‘books’ and ‘books/:id’ as endpoints. Non destructive actions (listing collections and listing a single resource) are triggered on a GET request and the destructive actions get called on the other requests (or just POST if method overloading is used). This prevents non-idempotent actions from using a GET request so mistakes like putting a delete button behind a get request are impossible.
    It also removes the need for HTTP method checking in the code so
    if ($this->_request->isPost()) isn’t needed.
  • Reuse
    When a public API is eventually needed the changes will be minimal.

The code snippet below is for an example restful BooksController extending the Zend_Rest_Controller. The Zend_Rest_Controller stubs out 5 abstract methods but if all the methods aren’t needed, the code still works if Zend_Controller_Action is extended.
The summary is that two resource endpoints are used:

  • ‘/books’ for a collection – a GET request lists the collection and a POST request adds to the collection)
  • ‘/books/:id’ for a single resource – a GET request lists the details for a single resource, a PUT request updates the resource and a DELETE request deletes the resource)

I have also added two helper methods to the controller – a newAction method to render the new book form and an editAction method to render the edit book form.

You can grab the full demo on github.

class BooksController extends Zend_Rest_Controller
{

	private $_booksTable;
	private $_form;

    public function init()
    {
        $bootstrap = $this->getInvokeArg('bootstrap');
        $db = $bootstrap->getResource('db'); 			

		$options = $bootstrap->getOption('resources');
		$dbFile  = $options['db']['params']['dbname'];
		if (!file_exists($dbFile)) {
		    $createTable = "CREATE TABLE IF NOT EXISTS books (
    					id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
    					name VARCHAR(32) NOT NULL,
					    price DECIMAL(5,2) NOT NULL
					)";
			$db->query($createTable);

			$insert1 = "INSERT INTO books (name, price) VALUES ('jQuery in Action', 39.99)";
			$insert2 = "INSERT INTO books (name, price) VALUES ('PHP in Action', 45.99)";
			$db->query($insert1);
	        $db->query($insert2);
		}

		$this->_booksTable = new Zend_Db_Table('books');
		$this->_form = new Default_Form_Book();
    }

    /**
     * The index action handles index/list requests; it should respond with a
     * list of the requested resources.
     */
    public function indexAction()
    {
        $this->view->books = $this->_booksTable->fetchAll();
    }

 	/**
     * The list action is the default for the rest controller
     * Forward to index
     */
    public function listAction()
    {
        $this->_forward('index');
    }

    /**
     * The get action handles GET requests and receives an 'id' parameter; it
     * should respond with the server resource state of the resource identified
     * by the 'id' value.
     */
    public function getAction()
    {
    	$this->view->book = $this->_booksTable->find($this->_getParam('id'))->current();
    }

	/**
     * Show the new book form
     */
    public function newAction() {
    	$this->view->form = $this->_form;
    }

    /**
     * The post action handles POST requests; it should accept and digest a
     * POSTed resource representation and persist the resource state.
     */
    public function postAction() {
    	if ($this->_form->isValid($this->_request->getParams())) {
    		$this->_booksTable->createRow($this->_form->getValues())->save();
       		$this->_redirect('books');
    	} else {
    		$this->view->form = $this->_form;
    		$this->render('new');
    	}
    }

 	/**
     * Show the edit book form. Url format: /books/edit/2
     */
    public function editAction() {
    	$book = $this->_booksTable->find($this->_getParam('edit'))->current();
    	$this->_form->populate($book->toArray());
    	$this->view->form = $this->_form;
    	$this->view->book = $book;
    }

    /**
     * The put action handles PUT requests and receives an 'id' parameter; it
     * should update the server resource state of the resource identified by
     * the 'id' value.
     */
    public function putAction() {
    	$book = $this->_booksTable->find($this->_getParam('id'))->current();
    	if ($this->_form->isValid($this->_request->getParams())) {
    		$book->setFromArray($this->_form->getValues())->save();
       		$this->_redirect('books');
    	} else {
    		$this->view->book = $book;
    		$this->view->form = $this->_form;
    		$this->render('edit');
    	}
    }

    /**
     * The delete action handles DELETE requests and receives an 'id'
     * parameter; it should update the server resource state of the resource
     * identified by the 'id' value.
     */
    public function deleteAction() {
    	$book = $this->_booksTable->find($this->_getParam('id'))->current();
    	$book->delete();
    	$this->_redirect('books');
    }

}

To get this working the following resource methods need to be added to the Bootstrap file.

    protected function _initRestRoute()
	{
		$this->bootstrap('Request');
		$front = $this->getResource('FrontController');
		$restRoute = new Zend_Rest_Route($front, array(), array(
		    'default' => array('books')
		));
		$front->getRouter()->addRoute('rest', $restRoute);
	} 

    protected function _initRequest()
    {
        $this->bootstrap('FrontController');
        $front = $this->getResource('FrontController');
        $request = $front->getRequest();
    	if (null === $front->getRequest()) {
            $request = new Zend_Controller_Request_Http();
            $front->setRequest($request);
        }
    	return $request;
    }

Restful controllers do add some much-needed consistency to Zend framework apps and hopefully we’ll start seeing them widely used real soon.

Zend_Db_Table update in ZF1.9

Sunday, July 26th, 2009

I usually have a bunch of empty Db_Table classes in my Zend Framework apps which just exist to define table names and do the inherited CRUD stuff.

However, with the just released version 1.9 of the framework (currently in beta), I don’t need to do this anymore. Zend_Db_Table is now a concrete implementation that accepts a table name as a parameter and can do all the CRUD stuff without needing a class to be defined.

Obviously, if custom code needs to be added to the Db_Table instance a class needs to be created but this will still go a long way towards cleaning up my models folder.

As an example:

  • The old way
    class Books extends Zend_Db_Table_Abstract {
    	protected $_name = 'books';
    }
    
    // and in the controller (or service layer class in my case)
    $booksTable = new Books();
    $books = $booksTable ->fetchAll();
    
  • The new way
    // no Db_Table subclass needed
    $booksTable = new Zend_Db_Table('books');
    $books = $booksTable ->fetchAll();
    

Another addition to Zend_Db is Zend_Db_Table_Definition and the concrete Zend_Db_Table constructor accepts an instance of this as a second parameter.
All setup code that can be added to an instance of Zend_Db_Table_Abstract (e.g. relationships) can be added through a definition class but the value of this is lost on me as this could lead to the same piece of code being defined in multiple places. In my opinion if the instance requires more than a table name, create a class for it.

Zend_Db_Table dynamic finders

Monday, June 15th, 2009

The recently added Zend_Navigation component uses dynamic finders to find pages e.g. findOneByLabel(‘Home’) to return the first matching page with label Home (and that’s straight from the manual).
It would be nice if Zend_Db_Table could do this too but it can’t. This seems a little inconsistent to me. Why do it for some components and not others?

Anyway, adding it wasn’t that difficult. The first step is to have an (abstract?) class extend Zend_DB_Table and let all your Db-backed models extend that. I’ll call mine App_Db_Table.

<?php

abstract class App_Db_Table extends Zend_Db_Table_Abstract {

	/**
	 * Db Instance
	 */
	protected $_db;

	/**
	 * Call method used to implement the dynamic finders
	 *
	 * @param string
	 * @param array
	 * @return function || void
	 */
	public function __call($method, $args) {
		$watch = array('findBy','findAllBy');
		foreach ($watch as $found) {
			if (stristr($method, $found)) {
				$fields = str_replace($found, '', $method);
				return $this->{'_' . $found}($fields, $args);
			}
		}
		throw new Exception("Call to undefined method App_Db_Table::{$method}()");
	}

	/**
     * Initialize object
     *
     * Called from {@link __construct()} as final step of object instantiation.
     */
    public function init() {
    	$this->_db = Zend_Registry::get('db');
    }

	/**
	 * Find By
	 *
	 * This method only ever returns the first record it finds!
	 *
	 * @param 	string
	 * @param 	array
	 * @return  object|false
	 */
	protected function _findBy($columns, $args) {
		$stmt = $this->_buildQuery($columns, $args);
		return $this->fetchRow($stmt);
	}

	/**
	 * Find All By
	 *
	 * @param 	string
	 * @param 	array
	 * @return	 object|false
	 */
	protected function _findAllBy($columns, $args) {
		$stmt = $this->_buildQuery($columns, $args);
		return $this->fetchAll($stmt);
	}

	/**
	 * Builds the query for the findBy methods
	 *
	 * @param 	string
	 * @param 	array
	 * @return 	Zend_Db_Select
	 */
	protected function _buildQuery($columns, $args) {
		$fields = explode('And', $columns);
		$count = count($fields);		

		$where = "{$this->_filterField($fields[0])} = ?";
		$where_args = $args[0];
		unset($args[0]);

		$select = $this->select();
		$select->where($where, $where_args);	

		if ($count > 1) {
			array_shift($fields);
			foreach ($fields as $field) {
				$where = "{$this->_filterField($field)} = ?";
				$where_args = array_shift($args);
				$select->where($where, $where_args);
			}
		}
		return $select;
	}

	/**
	 * Converts a camelCased word into an underscored word
	 *
	 * @param 	string
	 * @return  string
	 */
    protected function _underscore($word) {
    	$word = preg_replace('/([A-Z]+)([A-Z])/', '\1_\2', $word);
		return strtolower(preg_replace('/([a-z])([A-Z])/', '\1_\2', $word));
    }

    /**
     * Converts field name to lowercase and if camelcased, converts to underscored
     *
     * @param string
     * @return string
     */
    protected function _filterField($item) {
    	$item = $this->_underscore($item);
    	return strtolower($item);
    }

}

 

Usage

I’ll use a couple of unit tests to drive the class. I’ll assume a User class extends App_Db_Table. The table schema for my test is:

CREATE TABLE `users` (
`id` INT NOT NULL AUTO_INCREMENT PRIMARY KEY ,
`username` VARCHAR( 32 ) NOT NULL ,
`email` VARCHAR( 64 ) NOT NULL ,
`city` VARCHAR( 32 ) NOT NULL ,
`state` VARCHAR( 32 ) NOT NULL ,
`favourite_colour` VARCHAR( 16 ) NOT NULL
)

And the relevant section of the test class is as shown:

...
/**
 * Add  a user to the users table
 *
 * @param   array   $attributes
 */
private function _createUser($attributes = array()) {
    $details = array(
        'username'  => 'john',
        'email'     => 'john@example.com'
        'city'      => 'Colchester',
        'state'     => 'Essex',
        'favourite_colour' => 'red'
    );
    $params = array_merge($details, $attributes);
    $user = new User();
    $user->createRow($params)->save();
}

/**
 * Test findBy Dynamic finder with one parameter
 */
public function testFindByWithOneParameter() {
	$this->_createUser();
	$finder = new User();

	$found = $finder->findByEmail('john@example.com');
	$this->assertEquals(1, count($found));

	$found = $finder->findByEmail('smith@example.com');
	$this->assertFalse($found);
}

/**
 * Test findBy Dynamic finder with underscored column names
 */
public function testFindByWithUnderscoredColumnNames() {
	$this->_createUser();
	$finder = new User();

	$found = $finder->findByFavouriteColour('red');
	$this->assertEquals(1, count($found));
}

/**
 * Test findBy dynamic finder with more than one parameter
 */
public function testFindByWithMultipleParameters() {
	$this->_createUser();
	$finder = new User();

	$found = $finder->findByStateAndCity('Essex', 'Colchester');
	$this->assertEquals('Essex', $found->state);

	$found = $finder->findByStateAndCity('Essex', 'London');
	$this->assertFalse($found);

	$found = $finder->findByStateAndCity('London', 'Colchester');
	$this->assertFalse($found);
}

/**
 * Test findAllBy Dynamic finder with one parameter
 */
public function testFindAllByWithOneParameter() {
	$this->_createUser();
	$this->_createUser(array(
        'username'  => 'smith',
        'email'     => 'smith@example.com'
    ));

    $finder = new User();

	$found = $finder->findAllByState('Essex');
	$this->assertEquals(2, count($found));

	$found = $finder->findAllByUsername('john');
	$this->assertEquals(1, count($found));

	$found = $finder->findAllByEmail('smith');
	$this->assertEquals(0, count($found));
}
...

Zend Framework deployment with Capistrano

Friday, May 8th, 2009

After playing around with Phing for a while I decided to bring the simplicity of Capistrano – which I have been using with my Rails apps – to our Zend Framework project at work. Surprisingly it wasn’t hard at all.
“Capistrano is a tool for automating tasks on one or more remote servers”. That’s straight off their website and this tool rocks.

Capistrano is written in Ruby and needs the ruby library to be installed on the client machine. It does not need ruby on the production or any other target servers and at work we deploy from two machines and needed ruby installed on just the two. However the ruby files for the deployment recipes need to be in source control along with the rest of the code.
The following resources came in useful while we were setting up:

I use Ubuntu at work and the following steps are what we had to do to get Capistrano working on Ubuntu 8.04 but should work on most distros.

Step 1: Installing Ruby and Ruby gems

Install ruby packages

sudo aptitude install ruby1.8-dev ruby1.8 ri1.8 rdoc1.8 irb1.8 libreadline-ruby1.8 libruby1.8 libopenssl-ruby

Create some symlinks from the ruby install to their locations

sudo ln -s /usr/bin/ruby1.8 /usr/bin/ruby
sudo ln -s /usr/bin/ri1.8 /usr/bin/ri
sudo ln -s /usr/bin/rdoc1.8 /usr/bin/rdoc
sudo ln -s /usr/bin/irb1.8 /usr/bin/irb

Make sure subversion is installed

svn --version

If not installed, install it. You could of course use another source control system. Git seems to be the new hotness.

sudo apt-get install subversion

Install rubygems from source

mkdir ~/sources
cd ~/sources
w get http://rubyforge.org/frs/download.php/56227/rubygems-1.3.3.tgz
tar xzvf rubygems-1.3.3.tgz
cd rubygems-1.3.3
sudo ruby setup.rb

Create a symlink for rubygems and update it

sudo ln -s /usr/bin/gem1.8 /usr/bin/gem
sudo gem update
sudo gem update --system

Install capistrano, capistrano multi-stage and echoe gems

sudo gem install capistrano
sudo gem install capistrano-ext
sudo gem install echoe

That’s it. The Ruby environment is all set up.

Step 2: Creating the deploy file

Change to the root directory of your application, create a ‘config’ folder if you dont have one and type

capify .

Capistrano creates two files. A ‘Capfile’ for loading the deploy script and the deploy script itself. Open up the Capfile and remove the line where it tries to load plugins. This is specific to the Rails framework.
The Capfile should now look like this:

load 'deploy' if respond_to?(:namespace)
load 'config/deploy'

Open up the deploy.rb file created in the config folder and edit it for your setup.
The various configuration parameters are available on the Webistrano website. Our sample recipe is further down the page.

Step 3: Add multistage

For deploying to multiple environments, the capistrano multistage extension needs to be added. We usually have a staging, integration and a production stage and I’ll assume that here.
For multi-stage, the capistrano-ext gem needs to be included, the various stages specified and the default stage set. Furthermore, all configuration parameters not common to all stages need to be taken out of the deploy .rb file.

Create a deploy folder in the config directory and add a file for each of your environment. In our case we have production.rb, integration.rb and staging.rb files.
Add the environment specific parameters to these files.
Our sample deploy.rb file looks like:

set :stages, %w(staging integration production)
set :default_stage, "staging"
require 'capistrano/ext/multistage'

set :application, "app"  

#############################################################
#	Settings
#############################################################

default_run_options[:pty] = true
set :use_sudo, false

#############################################################
#	Servers
#############################################################

set :user, "username"
set :domain, "example.com"
set :port, 22
server domain, :app, :web
role :db, domain, :primary => true

#############################################################
#	Subversion
#############################################################

set :repository,  "http://example.com/svn/#{application}/trunk"
set :scm_username, "svn-user"
set :scm_password, "svn-pass"
set :deploy_via, :export

#############################################################
#	Tasks
#############################################################

after :deploy, 'deploy:cleanup'

# overide Rails specific tasks here.

desc "Do nothing"
deploy.task :restart, :roles => :app do
    # do nothing. php deploys don't need a server restart
end

deploy.task :migrate, :roles => :app do
    # migrate task is rails specific
end

Our staging deploy file sample (APPROOT/config/deploy/staging.rb)

set :deploy_to, "/path/to/staging"

If your staging and production environments are on different servers you obviously need to move the ‘Servers’ section to the individual deploy files.

Step 4: Setup and do initial deploy

The first step is to setup the server for Capistrano. Capistrano has a command to do this but due to the way it works, the application will be deployed to a folder called ‘current’ (really just a symlink to the latest release) within your ‘deploy_to’ directory. This means the document root (for apache virtualhost) will be in /path/to/staging/current/public rather than /path/to/staging/public and your apache (or nginx, etc.) config has to be changed to reflect this.

Commit the files to subversion (insert your SCM here) then type:

cap deploy:setup

This command creates two directories in your deploy_to path, releases and shared. The releases directory stores versions of the application as they are deployed. The”after :deploy, ‘deploy:cleanup’” line in the recipe cleans up this folder by keeping just the latest 5 (by default) releases. The ’shared’ folder is for storing images and other files that are not part of your application and are therefore not under source control.

To deploy your app, run:

cap deploy

This deploys to the default stage – staging in our case.
To deploy to production you’d type:

cap deploy production

It’s as easy as that. This is obviously not specific to the Zend Framework and can be used with any app.

Zend Framework and the cost of flexibility

Thursday, May 7th, 2009

I have had to downgrade my copy of the Zend Framework to 1.7.8 after 5 mins of ‘upgrading’ to 1.8.0 to get access to all the promised goodies (Zend Tool, Zend Application, Zend Navigation, etc).

Unfortunately, Zend_Loader::registerAutoload has been deprecated and I got notices all over my screen. Considering we are working on multiple projects in Zend Framework and we all have local copies I didn’t think it was such a good idea recommending this upgrade to the team especially with deadlines looming really close.

However, I don’t think I’ll be missing any of the new additions considering the hassle involved in using them. Why must everything be so complex in ZF? I copied over the CLI tool for  a test run but even that refused to run probably because I had an earlier version of the framework. I really want to love ZF (not that I have a choice, anyway) but with every new feature I think is cool comes the ‘why do I have to do all this to use it?’.

I had a look today at the Zend QuickStart to see if it had been updated to use Zend_Application and it has. However I still can’t figure out how this ‘new’ (more complex) way of setting up my application has any advantage over the ‘old’ way. I think I’ll stick with Jara. Furthermore, the ‘QuickStart’ is not really quick at all. What happened to the good old days when we built a blog in 20 mins without writing any code? Instead what we have is setting up cli tools, writing classes to initialise my app and an introduction to the Data Mapper, the most complex of Martin Fowler’s database access architectural patterns. I thought the quickstart was meant to sell the framework. This isn’t doing a very good job.

I’ll probably feel better tomorrow and get everyone to upgrade  but for now I guessed I’m pissed I can’t use the new stuff. However the truth still remains that ZF is not for the ‘lazy’. To use it well you’ve got to work hard and for a RAD tool that’s an irony.