Zend Framework Tutorial Series: Part 1 – The Module Based Application

These are a series of tutorials which are meant to show you or guide you through developing a complex application with Zend Framework 1.10.

1. Intro – The first part of the series

In this first article of the series, we will discuss about the best way (in my oppinion) to structure your Zend Application in order to have maximum flexibility but also a good defined structure of the classes/files.

These are a series of tutorials which are meant to show you or guide you through developing a complex application with Zend Framework 1.10.

The series consists of the following parts:
a) Setting up a module based application
b) Setting up helper plugins, methods & debugging with ZFDebug
c) Setting up a login page and signup page with captcha
d) Setting up OpenID to login/create account
e) Setting up an API to create/login an account
f) Improving performance implementing Zend Cache

You can download the whole package from Google Code Here.
Update: There was an issue with the Custom_Controller_Action_Abstract class with the $this->_redirect which was not initialized. The issue has been fixed below.

Update 2: After discussing with Paul via the comments below, I have updated the structure to be much more intuitive. Now the module library will be placed in the module folder itself proving a complete independence of the other modules.

I’ve also added some new features to the preDispatch from the Frontoffice Library Controller Abstract file that helps you set in the view namespaces for the FlashMessenger (which can be used like this:


$this->_helper->FlashMessenger->setNamespace(‘success’)->addMessage(‘The user has been created succesfully’);


You can still use the AjaxContextSwitch in your requests but it will not work if you mimin via the $_GET['ajax'] parameter

2. The WWW-ROOT

The first thing you need to do is get the Zend Library. I used Zend 1.10.4 for this tutorial.

It is security wise to have your public www-root somewhere else than your Zend library/Application files. Thus, I have the following structure for all my applications:

a) Path to the www-root : ex: /var/www/my_zend_example_application/
b) Path to the application: ex: /var/applications/my_zend_example_application/
c) Path to the Zend library: ex: /var/library/Zend/1.10.4/

Based on this list, create an index.php in your www-root folder with the following contents:

/**
 * Put errors on ON for debugging this file
 */
//ini_set('display_errors',1);
//error_reporting(E_ALL ^ E_DEPRECATED);

/*
 * Define the application environment
 */
define('APPLICATION_ENV', 'development');

/*
 * Defines the directory separator for windows or unix env
 */
define('DS', DIRECTORY_SEPARATOR);

/**
 * Define the absolute/relative paths to the library path, the app library path,
 * app path and the database configuration path
 */
define('ZEND_LIBRARY_PATH', realpath('/var/library/Zend/1.10.4'));
define('APPLICATION_PATH', '/var/applications/my_zend_example_application' );
define('APP_LIBRARY_PATH', APPLICATION_PATH . '/library');

$paths = array(
	ZEND_LIBRARY_PATH,
	APP_LIBRARY_PATH,
	get_include_path()
);

/**
 * Set the include paths to point to the new defined paths
 */
set_include_path(implode(PATH_SEPARATOR, $paths));

/** Zend_Application */
require_once 'Zend/Application.php';

// Create application, bootstrap, and run
$application = new Zend_Application(
    APPLICATION_ENV,
    APPLICATION_PATH . DS .  'config' . DS . 'application.ini'
);

//Start
$application->bootstrap();
$application->run();

Also, make sure you have the following .htaccess file in the same place as your index.php (in the www-root):

RewriteEngine On
RewriteRule !.(js|css|ico|gif|jpg|png)$ index.php
RewriteCond %{REQUEST_FILENAME} -s [OR]
RewriteCond %{REQUEST_FILENAME} -l [OR]
RewriteCond %{REQUEST_FILENAME} -d
RewriteRule ^.*$ - [NC,L]
RewriteRule ^.*$ index.php [NC,L]

This custom .htaccess will tell your http server to bypass Zend’s index.php for all requests for static files.

3. The Directory Structure

Once you have the library and the www-root defined, we will start setting up our application. Create the following directory structure in your application path (see above):

  • /config/
  • /library/
    • /Custom/
      • /Controller/
        • /Action/
          • /Helper/
        • /Plugin/
  • /modules/
    • /frontoffice/
      • /controllers/
      • /library/
        • /Controller/
          • /Action/
      • /layouts/
      • /models/
        • /Entities/
        • /Repositories/
      • /views/
        • /scripts/

Perhaps you are a bit confused now with the directory structure. But lets see exactly what happens:

We will have a common library for all our files named “Custom” which you can just reuse in any project you develop. This Custom library will hold some basic files which will be used by all modules. These files will do some auto magical logic to simplify your development.

We also have a module (only 1 for now) which will be defined as the default module. All modules will be found in the “modules” directory. Each module will use its own library. This way, all modules are 100% independent from each other.

The logic is the following:

A module controller extends the module’s library parent controller which also extends the Custom library controller.

Example:

class IndexController extends Frontoffice_Library_Controller_Action_Abstract { ... }

and

abstract class Frontoffice_Library_Controller_Action_Abstract extends Custom_Controller_Action_Abstract { ... }

This way, you can have logic applied to:
a) a particular controller
b) all module controllers
c) all application controllers

4. The Application.ini

Create a file named application.ini in your config folder from your application root with the following contents:

[bootstrap]
	Autoloadernamespaces[] = "Zend_"
	Autoloadernamespaces[] = "Custom_"

	resources.frontController.moduleDirectory = APPLICATION_PATH"/modules"
	resources.frontController.defaultModule = "frontoffice"
	resources.modules[] = ""
	resources.layout.layout = "layout"
	resources.layout.pluginClass = "Custom_Controller_Plugin_ModuleBasedLayout"
	resources.view.encoding = "UTF-8"
	resources.view.basePath = APPLICATION_PATH "/views/"

	bootstrap.path = APPLICATION_PATH "/Bootstrap.php"
	bootstrap.class = "Bootstrap"

	;Database settings
	resources.multidb.front_db.adapter  = "pdo_mysql"
	resources.multidb.front_db.host     = CHANGE_ME
	resources.multidb.front_db.username = CHANGE_ME
	resources.multidb.front_db.password = CHANGE_ME
	resources.multidb.front_db.dbname   = CHANGE_ME
	resources.multidb.front_db.default  = true

[production : bootstrap]

	resources.multidb.front_db.profiler.enabled = false
	resources.multidb.front_db.profiler.class   = "Zend_Db_Profiler_Firebug"

	phpSettings.display_startup_errors = 0
	phpSettings.display_errors         = 0
	settings.debug.enabled             = false

	settings.application.datetime = "Etc/GMT-8"

[qa : production]

	resources.multidb.front_db.profiler.enabled = false
	resources.multidb.front_db.profiler.class   = "Zend_Db_Profiler_Firebug"

	phpSettings.display_startup_errors = 0
	phpSettings.display_errors         = 0
	settings.debug.enabled             = false

	settings.application.datetime = "Etc/GMT-8"

[testing : qa]

	phpSettings.display_startup_errors = 0
	phpSettings.display_errors         = 0
	settings.debug.enabled = false

	settings.application.datetime = "Etc/GMT-8"

	resources.multidb.front_db.profiler.enabled = true
	resources.multidb.front_db.profiler.class   = "Zend_Db_Profiler_Firebug"

[development : testing]

	phpSettings.display_startup_errors = 1
	phpSettings.display_errors         = 1

	settings.application.datetime = "Europe/Bucharest"

	resources.multidb.front_db.profiler.enabled = true
	resources.multidb.front_db.profiler.class   = "Zend_Db_Profiler_Firebug"

The Database connection string is a global environment parameter (the host/dbname/user/password) which are constants defined in a separate DB.php file included from the index.php but is not in the scope of the article.

DEPRECATED: Moreover, we tell Zend to allow us to access our modules library via namespaces like “Frontoffice_Cool_Class”.

5. The Bootstrap.php and its module childs

In your application root, create a file called Bootstrap.php with the following contents:

class Bootstrap extends Zend_Application_Bootstrap_Bootstrap
{
	#stores a copy of the config object in the Registry for future references
	#!IMPORTANT: Must be runed before any other inits
	protected function _initConfig()
    {
    	Zend_Registry::set('config', new Zend_Config($this->getOptions()));
    }

	#Initializes the default timezone for the php ENV
	protected function _initDate()
    {
    	date_default_timezone_set(Zend_Registry::get('config')->settings
    														  ->application
    														  ->datetime);
    }

    #stores a copy of all the database adapters in the Registry for future references
	protected function _initDatabases()
    {
		$this->bootstrap('multidb');
		$resource = $this->getPluginResource('multidb');
    	$databases = Zend_Registry::get('config')->resources->multidb;
	    foreach ($databases as $name => $adapter)
	    {
	    	$db_adapter = $resource->getDb($name);
	    	Zend_Registry::set($name, $db_adapter);
	    }
    }
}

In your /modules/frontoffice/ create also a Bootstrap.php like this:

class Frontoffice_Bootstrap extends Zend_Application_Module_Bootstrap
{
        protected function _initLibraryAutoloader()
	{
		return $this->getResourceLoader()
					->addResourceType('library',
							 	   'library',
								   'library');
	}
}

You may ask yourself why have an empty module bootstrap (not empty anymore but the logic still remains) or if you will ever put code in your Module Bootstrap file:

The answer is no. Currently, Zend Framework loads ALL your bootstrap files, regardless of the module you are in. It is a complicated issue but in the end, it really doesn’t matter if you have code in your main bootstrap class or your module bootstrap class (actually, your module bootstrap class doesn’t have access to all items your main bootstrap class has). Moreover, for example, if you ignore the fact that zend loads all your bootstrap files, you might end up like me, having routes defined per module and then realizing that your routes for module A are being activated from module B resulting in a epic scale disaster :) .

It is true you can just put something like “if module name is “frontoffice” then run this bootstrap method else don’t” but what if you change the module name? It is an issue that I fixed having NO code in the module bootstrap class.

6. The Controller Plugins

In order to have a 100% no-dependency between modules (well 99% since modules are linked to the Custom library in the end – but you can just remove that link in less than 1 minute) you will need to have an individual module error controller and layouts path/files.

In order to do this, you will need to create the following 2 classes:

In your Application Root, in the /library/Custom/Controller/Plugin/ create the following file, ModuleBasedLayout.php with the following contents:

class Custom_Controller_Plugin_ModuleBasedLayout
	extends Zend_Layout_Controller_Plugin_Layout
{
	public function preDispatch(Zend_Controller_Request_Abstract $request)
    {
		$this->getLayout()->setLayoutPath(
			Zend_Registry::get('config')->resources->frontController->moduleDirectory
			. DS . $request->getModuleName() . DS . 'layouts' );
    }
}

It is very important, in order for this to work to NOT initialize your Layout Plugin in the bootstrap file rather then in your application.ini or else, your layout object won’tbe available and thus, will fail with a fatal error.

7. The Base Controller, the Module Base Controller and the Base Controller Action “Helper”

So, now we have almost everything ready. Let’s create our first controller: The IndexController.

In your application root, in the /modules/frontoffice/controllers create your IndexController.php file with the following contents:

class IndexController extends Frontoffice_Library_Controller_Action_Abstract
{
	public function indexAction()
	{
	}
}

Also, lets create the ErrorController also:

Class ErrorController extends Frontoffice_Library_Controller_Action_Abstract
{
	public function errorAction()
    {
		$error = $this->_getParam('error_handler');
        switch ($error->type) {
            case Zend_Controller_Plugin_ErrorHandler::EXCEPTION_NO_CONTROLLER:
            case Zend_Controller_Plugin_ErrorHandler::EXCEPTION_NO_ACTION:
                $this->getResponse()->setHttpResponseCode(404);
                $this->view->message = 'Uh oh, we can't seem to find that page you wanted!';
                $this->view->stack_trace = $this->_getFullErrorMessage($error);
				$this->view->code = 404;
                break;

            default:
                $this->getResponse()->setHttpResponseCode(500);
                $this->view->message = 'Looks like something's gone wrong! Please refresh the page - if the problem persists please report the error';
                $this->view->stack_trace = $this->_getFullErrorMessage($error);
				$this->view->code = 500;
                break;
        }

        $this->view->headTitle()->prepend( $this->view->code .  ' Error' );
    }

	protected function _getFullErrorMessage($error)
    {
		if (APPLICATION_ENV != 'development')
		{
			return '';
		}

        $message = '';

        if (!empty($_SERVER['SERVER_ADDR'])) {
            $message .= "Server IP: " . $_SERVER['SERVER_ADDR'] . "n";
        }

        if (!empty($_SERVER['HTTP_USER_AGENT'])) {
            $message .= "User agent: " . $_SERVER['HTTP_USER_AGENT'] . "n";
        }

        if (!empty($_SERVER['HTTP_X_REQUESTED_WITH'])) {
            $message .= "Request type: " . $_SERVER['HTTP_X_REQUESTED_WITH'] . "n";
        }

        $message .= "Server time: " . date("Y-m-d H:i:s") . "n";
        $message .= "RequestURI: " . $error->request->getRequestUri() . "n";

        if (!empty($_SERVER['HTTP_REFERER'])) {
            $message .= "Referer: " . $_SERVER['HTTP_REFERER'] . "n";
        }

        $message .= "Message: " . $error->exception->getMessage() . "nn";
        $message .= "Trace:n" . $error->exception->getTraceAsString() . "nn";
        $message .= "Request data: " . var_export($error->request->getParams(), true) . "nn";

        $it = $_SESSION;

        $message .= "Session data:nn";
        foreach ($it as $key => $value) {
            $message .= $key . ": " . var_export($value, true) . "n";
        }
        $message .= "n";

        $message .= "Cookie data:nn";
        foreach ($_COOKIES as $key => $value) {
            $message .= $key . ": " . var_export($value, true) . "n";
        }
        $message .= "n";

        return '< pre>' . $message . '< / pre>';
    }
}

This error controller will basically allow you see a simple nice looking error message in your live/production environment but in development, lets you see full information about your active session (or the users one).

This can be extended to mail you the error details, get database query profiles and so on and so on.

Now, that we have these controllers, lets see what do they extend: The Module Base Controller:

In your application root, in the modules/frontoffice/library/Controller/Action/ create a file called Abstract.php with the following contents:

abstract class Frontoffice_Library_Controller_Action_Abstract
	extends Custom_Controller_Action_Abstract
{
	public function init()
	{
		$this->_initView();
	}

	/**
	 * Before dispatching the requested controller/action
	 * check to see if teh request is an AJAX request (via XMLHTTPREQUEST or $_GET['ajax']
	 *
	 * If it is an ajax request, remove the layout
	 *
	 * If it is not, setup the FlashMessenger
	 */
	public function preDispatch()
	{
		//if  its an AJAX request stop here - can be simulated via ?ajax GET parameter sent in the request
		if ($this->_request->isXmlHttpRequest() || isset($_GET['ajax']))
		{
			Zend_Controller_Action_HelperBroker::removeHelper('Layout');
		}

		if (!$this->getRequest()->isXmlHttpRequest())
		{
			$messages = array();
			$messages['error']   = $this->_helper->FlashMessenger->setNamespace('error')->getMessages();
			$messages['success'] = $this->_helper->FlashMessenger->setNamespace('success')->getMessages();
			$this->view->messages = $messages;
		}

		//Sets the base url to the javascripts of the application
		$script = '
			var base_url = "' . $this->view->baseUrl() . '";
		';
		$this->view->headScript()->prependScript($script, $type = 'text/javascript', $attrs = array());
	}

    protected function _initView()
    {
    	$view = new Custom_Controller_Action_Helper_View($this->view);
		$this->view = $view->init();
    }
}

The Init View method instantiates and runs the following class/object:

In your application root, /library/Custom/Controller/Action/Helper create a View.php with the following contents:

class Custom_Controller_Action_Helper_View
{
	public $view;

	public function __construct($view)
	{
		$this->view = $view;
	}

	public function init()
	{
    	// set encoding and doctype
		$this->view->setEncoding('UTF-8');

		$this->view->doctype('XHTML1_STRICT');

		// set the content type and language
		$this->view->headMeta()
			       ->appendHttpEquiv('Content-Type', 'text/html; charset=UTF-8');

		$this->view->headMeta()
				   ->appendHttpEquiv('Content-Language', 'en-US');

		// setting the site in the title
		$this->view->headTitle('My Cool Application');
		//	setting a separator string for segments:
		$this->view->headTitle()->setSeparator(' - ');

		return $this->view;
	}
}

Lastly, lets see the Global Base Controller. In your application root, in /library/Custom/Controller/Action/ create a file called Abstract.php with the following contents:

abstract class Custom_Controller_Action_Abstract extends Zend_Controller_Action
{
	/**
	 * Helper method to redirect to a specific action or controller from a
	 * specific module, via a specified route(or not) with specified parameters
	 *
	 * @param string $controller / $url which contains http in its composition
	 * @param string $action
	 * @param string $module
	 * @param array  $params
	 * @param string $route
	 * @param boolean $reset
	 */
	public function redirect($controller = 'index', $action = 'index', $module = 'frontoffice', $params = array(), $route = null, $reset = true )
    {
        $this->_redirect = $this->_helper->getHelper('Redirector');

    	$current_controller = $this->_getParam('controller');
    	$current_action     = $this->_getParam('action');
    	$current_module     = $this->_getParam('module');

    	if ($current_controller == $controller &&
    		$current_action == $action &&
    		$current_module == $module)
    	{
    		return TRUE;
    	}

    	if (strstr($controller, 'http'))
    	{
    		return $this->_redirect($controller, array('code' => 301));
    	}

    	if ($route !== null)
    	{
    		$params = array_merge(array('action'     => $action,
							    	    'controller' => $controller,
                               			'module'     => null), $params);

    		return $this->_redirect->setCode(301)
    		                       ->setExit(true)
    		                       ->gotoRoute($params, $route, $reset);
    	}

	    return $this->_redirect->setCode(301)
	    			    	   ->setExit(true)
	                   		   ->gotoSimpleAndExit($action,
	                                           	   $controller,
	                                           	   $module,
	                                           	   $params);
    }
}

You can use the method “redirect” anywhere in your controller to redirect either to a module/controller/action with params and with a route or without a route OR to a certain url :

Example:

...
return $this->redirect('http://google.com');

or

return $this->redirect('controller', 'action', 'module', arrary('param1' => 'value'), 'my-cool-route');

8. The Layout & Views

The last thing before testing that everything works is to create a view and layout for your applicatio module:

Create in your application root /modules/frontoffice/layouts a file called layout.phtml with the following contents:

< ? php echo $this->doctype(); ? >
< html xmlns="http://www.w3.org/1999/xhtml" lang="en" xml:lang="en">
< head>
	< ? php echo $this->headMeta(); ?>
	< ? php echo $this->headTitle(); ?>
	< ? php echo $this->headStyle(); ?>
	< ? php echo $this->headLink(); ?>
	< script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.4.2/jquery.min.js">
	< ? php echo $this->headScript(); ?>

< body>
	< div class="container">
	< ? php echo $this -> layout () -> content; ?>
	< /div>
< /body>
< /html>

Create in your application root /modules/frontoffice/views/scripts/index a file called index.phtml with the following contents:

Hello Frontoffice Index Controller World!

Now you can test your application by going to your www-root.

9. Module Models

Create in your application root /modules/frontoffice/models/Entities/ a file called Test.php – singular since it is an entity of the Test object – with the following contents:

class Frontoffice_Model_Entities_Test
	//extends Zend_Db_Table_Abstract
{
	public function test($message)
	{
		return 'Hello ' . $message;
	}
}

Create in your application root /modules/frontoffice/models/Repositories/ a file called Tests.php – plural since its a repository of the Tests object – with the following contents:

class Frontoffice_Model_Repositories_Tests
{
	public function test($message)
	{
		if (TRUE === empty($message))
		{
			throw new Zend_Exception('Invalid Message Provided to the Test Object');
		}

		$test_entity = new Frontoffice_Model_Entities_Test();
		return $test_entity->test($message);
	}
}

Now in your IndexController use the following code to test your model:

...
$tests_repository = new Frontoffice_Model_Repositories_Tests();
$this->view->message = $tests_repository->test('Andrew');

In your index.phtml use the following code to display your Model result:

...
echo $this->message;

If everything is setup correctly, you should see a message like “Hello Andrew” on your screen.

10. Adding more modules

The basic ideea of adding more modules to the application at this point is by adding 1 directory per module in your application root, in the /modules directory, using the same directory structure as the frontoffice module but with the following mandatory rule:

All modules that are NOT your default module, need to have the following namespace in their controllers: Modulename_ControllerName

Example: For an Admin module, you would have Admin_IndexController, Admin_UsersController and so on.

DEPRECATED: You will need to have a library for each module, in your library folder, just like the Frontoffice library and that is about it.

11. Conclusion

This basic setup should get you started in our tutorials series with our complex application.
Next tutorial will consist of adding a complex debugging tool via ZFDebug customized + other helper methods.

You can download the example application here.

See you next time!