Zend_Rest - Example

I built a REST API and calling code using Zend Framework. This code also uses Doctrine (http://www.doctrine-project.org/projects/orm).

Prior to coding, I reviewed several resources on the ‘net in an attempt to follow best practices. I visited the following, as well as others:

routes.ini

These settings ensure the request is properly organized when received by the REST controller. Be sure the ACL is configured to allow access as appropriate as well.

# REST routes
resources.router.routes.api.type = "Zend_Rest_Route"
resources.router.routes.api.route = "/api/:id"
resources.router.routes.api.defaults.module = "api"
resources.router.routes.api.defaults.controller = "index"
resources.router.routes.api.api = "admin, vendor, client"

Calling Code

The calling code is a subset, only the relevant code is included.

Authentication is handled by JSON encoding, then encrypting the username and password in the Authorization header.

The process has two phases, a GET to test whether the element exists and a POST, PUT or DELETE depending on existence, error, or requested action. Since the GET is a test, no data is returned. POST and PUT pass the data in the body text, JSON encoded.

	
            $client = new Zend_Http_Client();
            $iv_size = mcrypt_get_iv_size(MCRYPT_RIJNDAEL_256, MCRYPT_MODE_ECB);
            $iv = mcrypt_create_iv($iv_size, MCRYPT_RAND);
            $authorization = base64_encode(mcrypt_encrypt(MCRYPT_RIJNDAEL_256, $v['url'],
                Zend_Json::encode(array('username'=>$this->username,'password'=>$this->password)),
                MCRYPT_MODE_ECB, $iv));
            $client->setHeaders(array('keepalive' => true,'Authorization' => $authorization));
            $client->setUri($api['protocol'].'://'.$url.'/'.$api['path'].'/admin/'.$admin->uuid);
			$client->setMethod(Zend_Http_Client::GET);
			$response = $client->request();
			$status = $response->getStatus();
			$body = $response->getBody();
			switch ($status)
			{
				case 200:	/* Record exists */
					$next_method = ($type == 'allow') ? Zend_Http_Client::PUT : Zend_Http_Client::DELETE;
					break;
				case 404:	/* Record does not exist */
					if ($type == 'allow')
					{
						$client->setUri($api['protocol'].'://'.$url.'/'.$api['path'].'/admin');
						$next_method = Zend_Http_Client::POST;
						break;
					}
				default:	/* Error */
					return false;
			}
	
			if (($next_method == Zend_Http_Client::POST) ||
				($next_method == Zend_Http_Client::PUT))
				{
					$values = array('uuid','password','email','first_name','middle_name','last_name','status','last_modified_by');
					$admin_data = $admin->getData();
					foreach ($values as $k => $v)
						$data[$v] = $admin_data[$v];
					$jsonData = Zend_Json::encode($data);
					$client->setRawData($jsonData,'text/json');
				}
		
			$client->setMethod($next_method);
			$response = $client->request();
			$status = $response->getStatus();
			$client->getAdapter()->close();
			$return = in_array($status,array(200,201,204));

modules/api/controllers/AdminController.php

The REST controller uses a preDispatch override to ensure the submitted id, username and password are valid. Invalid requests are discarded with an HTTP/400. Unauthorized requests receive an HTTP/403.

GET requests use the submitted UUID to test for the existence of the data. Using a UUID ensures the data will be identified the same across different systems. The data has local identifiers as well, and some translation is applied, although it has been removed from the posted code.

The JSON data is read from the request body with a file_get_contents on php://input

<?php
class Api_AdminController extends Zend_Rest_Controller
{
	private $model = null;
	private $uuid = null;
	private $data = null;
	private $response = null;

	public function init()
	{
		$this->apiBaseUrl = $_SERVER['SERVER_NAME'].'/api/admin';
		$this->_helper->layout->disableLayout();
		$this->getHelper('viewRenderer')->setNoRender(true);
	}

	public function preDispatch()
	{
		// This ensures the required data is present and valid prior to processing any requests
		$hostname = new Zend_Validate_Hostname(array('allow'=>Zend_Validate_Hostname::ALLOW_DNS));
		$validators = array(
			'uuid'=>array(array('Regex','pattern' => '/^[\da-f]{8}-([\da-f]{4}-){3}[\da-f]{12}$/','allowEmpty'=>false,'presence'=>'required')),
			'username'=>array(array('EmailAddress','allowEmpty'=>false,'presence'=>'required','allow' => $hostname,'mx' => true)),
			'password'=>array(array('StringLength','min'=>8,'max'=>32)));
		$input = new Zend_Filter_Input(array(),$validators);
		$request = $this->getRequest();

        $this->data = Zend_Json::decode(file_get_contents('php://input'));
        if ($request->isPost())
            $this->uuid = $this->data['uuid'];
        else
        {
            $this->uuid = $request->getParam('id');
            $this->data['uuid'] = $this->uuid;
        }

        $iv_size = mcrypt_get_iv_size(MCRYPT_RIJNDAEL_256, MCRYPT_MODE_ECB);
        $iv = mcrypt_create_iv($iv_size, MCRYPT_RAND);
        $authorization = Zend_Json::decode(trim(mcrypt_decrypt(MCRYPT_RIJNDAEL_256, $_SERVER['SERVER_NAME'],
            base64_decode($request->getHeader('Authorization')),
            MCRYPT_MODE_ECB, $iv)));
        $input->setData(array(
            'uuid'=>$this->uuid,
            'username'=>$authorization['username'],
            'password'=>$authorization['password']));

		if (!$input->isValid())
			$this->error(400);
		else
		{
			// Data is valid, authenticate based on username and password
		        $session = new Zend_Session_Namespace('global');
        		$adapter = new Application_Auth_Adapter_Doctrine($input->username,$input->password);
			$auth = Zend_Auth::getInstance();
			$result = $auth->authenticate($adapter);
	        	if (!$result->isValid()) 
				$this->error(403);
			else
			{
				// Prepare the response object
				$api = Zend_Registry::get('bootstrap')->getOption('rest');
				$this->response = array(
					'link'=>$api['protocol'].'://'.$this->apiBaseUrl.'/'.$input->id,
					'title'=>'Admin',
					'charset'=>'utf-8');
			}
		}

		$session=new Zend_Session_Namespace('global');
		switch ($session->type)
		{
			case 'client': 
				$this->model = 'Application_Model_Admin'; 
				break;
			case 'vendor': 
				$this->model = 'Application_Model_VendorAdmin'; 
				break;
			case 'admin': 
				$this->model = 'Application_Model_Admin'; 
				break;
			default: 
				$this->error(400);
				break;
		}
	}
	
	public function indexAction()
	{
		// Handles GET requests
	}
	
	public function getAction()
	{
		// Handles GET requests
		$q = Doctrine_Query::create()
			->from ($this->model)
			->where('uuid = ?',$this->uuid);
		$result = $q->fetchArray();
		$q->free();
		switch (count($result))
		{
			case 1:
				// The data is NOT sent back to the client.  The response serves only to acknowledge the existence of the record
				// In this application, the GET request is really querying for presence, not requesting data
				$this->response['updated'] =$result[0]['updated_at'];
				$this->_helper->json($this->response);
				break;
			case 0:
				$this->error(404);
				break;
			default:
				// Database should only have one instance of UUID
				$this->error(409,'Multiple matches - contact support');
				break;
		}
	}

	private function process()
	{
		$form = new Application_Access_Admin_View_Form();
		$form->populate($this->data);
		$values = array('uuid','password','email','first_name','middle_name','last_name','status','last_modified_by');

		$data = array();
		foreach ($elements as $k => $v)
		{
			$name = $v->getName();
			if (!in_array($name,$values))
				$form->removeElement($name);
			else
				$data[$name] = $v->getValue();
		}

		if ($form->isValidPartial($data))
		{
			$request = $this->getRequest();
			if (!$request->isPost())
			{
				$q = Doctrine_Query::create()
					->from ($this->model)
					->where('uuid = ?',$this->uuid);
				$admin = $q->fetchOne();
				$q->free();
			}
			else
			{
				$admin = new $this->model;
				$admin->assignDefaultValues();
			}
			$admin->fromArray($form->getValues());
			$response = $this->getResponse();
			try
			{
				$admin->save();
				if ($request->isPost())
					$response->setHttpResponseCode(201);
				else
					$response->setHttpResponseCode(204);
			}
			catch(Doctrine_Exception $e)
			{
   				$this->error(400,var_export($admin->getErrorMessages(),true));
			}
			catch (Exception $e)
			{
   				$this->error(400,var_export($e->getMessage(),true));
			}
		}
		else
   			$this->error(400,var_export($form->getMessages(),true));
	}

	public function postAction()
	{
		$this->process();
	}

	public function putAction()
	{
		$this->process();
	}
	
	public function deleteAction()
	{
		$q = Doctrine_Query::create()
			->from ($this->model)
			->where('uuid = ?',$this->uuid);
		$result = $q->fetchOne();
		$q->free();
		$result->delete();
		$response = $this->getResponse();
		$response->setHttpResponseCode(204);
	}

	private function error($code,$message=null)
	{
		// If a message is returned, it is text in the body
		$response = $this->getResponse();
		$response->setHttpResponseCode($code);
		if ($message != null)
			$response->setBody($message.PHP_EOL.PHP_EOL);
		$response->sendResponse();
		exit;
	}
}

Use curl to test the interface, or PHP.