Tutorials

An Example Zend Framework Blog Application – Part 5: Creating Models with Zend_Db and adding an Administration Module

Oct 06, 2008 insic 8 Comments

Step 1: Creating a Database Schema

The schema for our blog’s database will be aimed at MySQL. We’re only starting with two tables to hold entries and authors, which may appear a little suspicious. The suspicion of course is due to the lack of two elements: Authentication and Authorisation. We’ll cover both in Part 6 in the near future.

Here’s the tables I came up with for now – ensure you have a local database set up, perhaps using phpMyAdmin, so you can just throw in this SQL to create the tables.

CREATE TABLE `entries` (
`id` int(11) NOT NULL auto_increment,
`title` varchar(200) collate utf8_unicode_ci NOT NULL,
`date` timestamp NOT NULL default ’0000-00-00 00:00:00′,
`author` varchar(60) collate utf8_unicode_ci NOT NULL default ‘anonymous’,
`author_id` int(11) NOT NULL default ’0′,
`body` text collate utf8_unicode_ci NOT NULL,
`extended_body` text collate utf8_unicode_ci NOT NULL,
`comment_count` int(4) NOT NULL default ’0′,
`last_modified` timestamp NOT NULL default CURRENT_TIMESTAMP on update CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
FULLTEXT KEY `title` (`title`),
FULLTEXT KEY `body` (`body`),
FULLTEXT KEY `extended_body` (`extended_body`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;

CREATE TABLE `authors` (
`id` int(11) NOT NULL auto_increment,
`realname` varchar(255) collate utf8_unicode_ci NOT NULL,
`username` varchar(20) collate utf8_unicode_ci NOT NULL,
`password` varchar(64) collate utf8_unicode_ci NOT NULL,
`email` varchar(128) collate utf8_unicode_ci NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;

We’ll likely only use the bare minimum of fields to start with. There are others such as comment_count in entries we can’t use yet since we don’t have a commenting system in place, and likely more just not included above yet. Obviously the input page for entries can be anything from a simple set of form fields, to a three page monstrosity!

If you’re curious about why we’re using MyISAM for the entries table instead of InnoDB as for authors, the simple reason is that full-text indices are only available on MyISAM tables to enable search functionality using the MATCH() function in MySQL. We’ve added full-text indices for the title, body, and extended_body fields.

Step 2: Adding the Models

To interact with our newly created database tables, we’re going to need some Models. Back in our previous excursion into explaining Model-View-Controller (MVC) we described that a Model is basically a representation of application state – i.e. data persisted between requests often in a database. The Model for a database in the Zend_Framework is usually captured as a Class inheriting from Zend_Db_Table.

The Model for our entries table will be maintained in an Entries class in /application/models/Entries.php:

<?php

class Entries extends Zend_Db_Table
{

    protected $_name = ‘entries’;

}

Similarly we will have /application/models/Authors.php:

<?php

class Authors extends Zend_Db_Table
{

    protected $_name = ‘authors’;

}

It really is that simple for now. Zend_Db_Table even assumes the primary key is “id” unless otherwise defined. If you really want to know what the fuss about Models is, it’s all in the methods you add to this class. For now, it’s a blank slate referencing the database’s table name. But you can imagine a few business logic rules you could consider adding here (hint: processing of entry bodies before saving using HTMLPurifier). The main idea to keep in mind is that if you’re doing something to data from a Model, and it’s potentially useable in more than one Controller, then chances are you’re better off adding it as a Model method. Why make your Controllers fat and bloated? The more bloat they contain, the harder it becomes to see the application’s workflow by reading them!

Step 3: Adding Database Credentials and a Default Database Adapter for Zend_Db

We have our database schema in place along with Models to represent it. Time to do something so our Model can actually interact with the database then :-).

The first step is storing our database credentials in an editable file. Create a new file called config.ini in /config containing something along the lines of the following (edit for your personal credentials and database name):

[general]

;Database connection settings
db.adapter=PDO_MYSQL
db.host=localhost
db.username=root
db.password=passwd
db.dbname=zfblog

Note: The Subversion repository contains a template of the above file called config.ini.example. This way I can change my own config.ini file (which is on the subversion ignore list for its parent directory) without committing its changes to the repository all the time! You will need to manually create a copy if running from Subversion.

By now you probably know the drill, and you’re wondering just how far I mangled /application/Bootstrap.php to integrate database setup…

Not too much really. In the below revised Boostrap file I’ve shuffled a few things around, added new methods for setting up a Registry, Config file and Database connection. Hopefully it’s self explanatory by now… To answer a question raised in the comments previously, I’m using a static class simply because the Bootstrap isn’t really responsible for a whole lot other than initialisation and execution – most tutorials fall so far back in time they even use procedural style PHP! ;-) Static methods are also attractive because I do rely on not needing a discrete instance for other reasons – which we’re not covering here since I’m not obsessing about Behaviour-Driven Development or TDD in this series.

<?php

require_once ‘Zend/Loader.php’;

class Bootstrap
{

    public static $frontController = null;

    public static $root = ;

    public static $registry = null;
   
    public static function run()
    {
        self::prepare();
        $response = self::$frontController->dispatch();
        self::sendResponse($response);
    }
   
    public static function setupEnvironment()
    {
        error_reporting(E_ALL|E_STRICT);
        ini_set(‘display_errors’, true);
        date_default_timezone_set(‘Europe/London’);
        self::$root = dirname(dirname(<u>_FILE_</u>));
    }
   
    public static function prepare()
    {
        self::setupEnvironment();
        Zend_Loader::registerAutoload();
        self::setupRegistry();
        self::setupConfiguration();
        self::setupFrontController();
        self::setupView();
        self::setupDatabase();
    }
   
    public static function setupFrontController()
    {
        self::$frontController = Zend_Controller_Front::getInstance();
        self::$frontController->throwExceptions();
        self::$frontController->returnResponse(true);
        self::$frontController->setControllerDirectory(
            self::$root . ‘/application/controllers’
        );
        self::$frontController->setParam(‘registry’, self::$registry);
    }
   
    public static function setupView()
    {
        $view = new Zend_View;
        $view->setEncoding(‘UTF-8′);
        $viewRenderer = new Zend_Controller_Action_Helper_ViewRenderer($view);
        Zend_Controller_Action_HelperBroker::addHelper($viewRenderer);
        Zend_Layout::startMvc(
            array(
                ‘layoutPath’ => self::$root . ‘/application/views/layouts’,
                ‘layout’ => ‘common’
            )
        );
    }
   
    public static function sendResponse(Zend_Controller_Response_Http $response)
    {
        $response->setHeader(‘Content-Type’, ‘text/html; charset=UTF-8′, true);
        $response->sendResponse();
    }

    public static function setupRegistry()
    {
        self::$registry = new Zend_Registry(array(), ArrayObject::ARRAY_AS_PROPS);
        Zend_Registry::setInstance(self::$registry);
    }

    public static function setupConfiguration()
    {
        $config = new Zend_Config_Ini(
            self::$root . ‘/config/config.ini’,
            ‘general’
        );
        self::$registry->configuration = $config;
    }

    public static function setupDatabase()
    {
        $config = self::$registry->configuration;
        $db = Zend_Db::factory($config->db->adapter, $config->db->toArray());
        $db->query(“SET NAMES ‘utf8′”);
        self::$registry->database = $db;
        Zend_Db_Table::setDefaultAdapter($db);
    }
   
}

The setupDatabase() method takes the new Zend_Config instance storing our configuration data from config.ini (and which we’re now keeping in the Registry for reference) and sets up a connection by passing the configuration data to Zend_Db’s factory() static method. The first act is to use the new connection to ensure we’re once again being careful to stick with UTF-8 encoding. Have to be careful with my name afterall – it has one of those weird European slash thingies over the a (we call it the “fada” in Irish Gaelic, the French call it an “acute”, and HTML standards refer to it as “á” – outnumbered two to one ;-)).

The connection object is stored to the Registry in case it’s required later. It’s usually needed as a last resort if we need it for something a Model isn’t well suited for. The Registry is passed to the Front Controller in setupFrontController() as a user parameter. Finally, but not least, we make this new connection the default for Zend_Db_Table – available to all our Models.

It does look like a complicated web of steps, but honestly the code is pretty small for all this.

Step 4: Adding an Administration Module

We have the database, the Model, and the database connection.

Before we jump further, I’m going to make an assumption. Entry submissions will only be allowed from an Administration Module. Once we do have Authorisation implemented we’ll seal off access to any such Administration functions, but for now since the blog remains on our private development PC we can leave it openly accessible – until Part 6 ;-).

The Zend Framework allows for all Controllers and Views to be grouped into Modules relatively easily. Up to now we’ve been putting everything into their default locations without any thought of Modules, so it’s high time we added one for Administration.

The first step is to introduce a new directory to our current directory structure called admin inside /application. You can also delete the previously suggested modules directory – we’ll keep the directory tree a little flatter than I usually go with, although I can change this if readers prefer. Inside admin, add both a controllers and views directory. The views directory should in turn have filters, helpers, layouts and scripts directories.

To allow our application divert requests to the new admin module, we also need to register its controllers directory with the Front Controller. This is a quick edit to our Bootstrap file at /application/Boostrap.php in the setupFrontController() method:

<?php

require_once ‘Zend/Loader.php’;

class Bootstrap
{

    public static $frontController = null;

    public static $root = ;

    public static $registry = null;
   
    public static function run()
    {
        self::prepare();
        $response = self::$frontController->dispatch();
        self::sendResponse($response);
    }
   
    public static function setupEnvironment()
    {
        error_reporting(E_ALL|E_STRICT);
        ini_set(‘display_errors’, true);
        date_default_timezone_set(‘Europe/London’);
        self::$root = dirname(dirname(<u>_FILE_</u>));
    }
   
    public static function prepare()
    {
        self::setupEnvironment();
        Zend_Loader::registerAutoload();
        self::setupRegistry();
        self::setupConfiguration();
        self::setupFrontController();
        self::setupView();
        self::setupDatabase();
    }
   
    public static function setupFrontController()
    {
        self::$frontController = Zend_Controller_Front::getInstance();
        self::$frontController->throwExceptions();
        self::$frontController->returnResponse(true);
        self::$frontController->setControllerDirectory(
            array(
                ‘default’ => self::$root . ‘/application/controllers’,
                ‘admin’ => self::$root . ‘/application/admin/controllers’
            )
        );
        self::$frontController->setParam(‘registry’, self::$registry);
    }
   
    public static function setupView()
    {
        $view = new Zend_View;
        $view->setEncoding(‘UTF-8′);
        $viewRenderer = new Zend_Controller_Action_Helper_ViewRenderer($view);
        Zend_Controller_Action_HelperBroker::addHelper($viewRenderer);
        Zend_Layout::startMvc(
            array(
                ‘layoutPath’ => self::$root . ‘/application/views/layouts’,
                ‘layout’ => ‘common’
            )
        );
    }
   
    public static function sendResponse(Zend_Controller_Response_Http $response)
    {
        $response->setHeader(‘Content-Type’, ‘text/html; charset=UTF-8′, true);
        $response->sendResponse();
    }

    public static function setupRegistry()
    {
        self::$registry = new Zend_Registry(array(), ArrayObject::ARRAY_AS_PROPS);
        Zend_Registry::setInstance(self::$registry);
    }

    public static function setupConfiguration()
    {
        $config = new Zend_Config_Ini(
            self::$root . ‘/config/config.ini’,
            ‘general’
        );
        self::$registry->configuration = $config;
    }

    public static function setupDatabase()
    {
        $config = self::$registry->configuration;
        $db = Zend_Db::factory($config->db->adapter, $config->db->toArray());
        $db->query(“SET NAMES ‘utf8′”);
        self::$registry->database = $db;
        Zend_Db_Table::setDefaultAdapter($db);
    }
   
}

Nothing huge has changed. The only odd part perhaps, is that our previous controller directory is now assigned as a “default”. This is a special Module key and you should never omit it when setting up Modules since it is the ultimate fallback position for failed requests (after that it’s to the ErrorController we’ll add in the next Part of this tutorial series).

Our Administration module is going to have two controllers. An Index Controller which will default to displaying a list of available actions (e.g. create new blog entry), and an Entry Controller for adding new Entries (and later editing and deleting them). We’ll create a new Index Controller at /application/admin/controllers/IndexController.php containing the following:

<?php

class Admin_IndexController extends Zend_Controller_Action
{

    public function indexAction()
    {
    }

}

You’ll notice that all Controllers not part of the default Module must have their class name prefixed with the Module name followed by an underscore. This merely ensures we have no annoying name conflicts between Modules over common Controller names. The indexAction method is completely empty – on purpose. The controller has nothing to do here except look pretty for a few microseconds until control passes to the ViewRenderer Action Helper to have the correct View rendered.

The most obvious next step, therefore, is adding a template for our new module’s index page. Create index.phtml at /application/admin/views/scripts/index/index.phtml and we’ll just add, for now, a simple listing of available functions. All one of them.

The first one we need of course, is a URL for adding a new Blog entry. We’ll assume we’re going to use an Entry Controller within the Admin Module:

<h2>Administration</h2>

<ul>
<li><a href=“/admin/entry/add”>New Entry</a></li>
</ul>

Note: Please do not forget the leading forward slash in relative URLs! That little character ensures all relative URLs remain relative to the base URL and not just get appended to the current URL (which is all prettied up).

Try opening up our budding Administration Panel using the URL http://zfblog/admin.

There’s one small niggle here, which demonstrates another interesting facet of using Zend_Layout. The index page for Administration has that text filled right column. We don’t need that – it’s destined to hold Blog related stuff for public consumption. Let’s make our Admin module utilise a slightly different layout! We’ll add a narrower left column this time for future Administration functions.

Step 5: Adding an Administration Module specific Layout

So, add a new template admin.phtml to /application/admin/views/layouts/admin.phtml. This will be an exact duplicate of /application/views/layouts/common.phtml with a few column changes.

<!DOCTYPE html PUBLIC “-//W3C//DTD XHTML 1.0 Strict//EN”
  “http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd”>
<html xmlns=“http://www.w3.org/1999/xhtml” xml:lang=“en” lang=“en”>
<head>
    <meta http-equiv=“Content-Type” content=“text/html; charset=utf-8″ />
    <meta name=“language” content=“en” />
    <title><?php echo $this->escape($this->title) ?></title>
    <link rel=“stylesheet” href=“/css/blueprint/screen.css” type=“text/css” media=“screen, projection”>
    <link rel=“stylesheet” href=“/css/style.css” type=“text/css” media=“screen, projection”>
    <link rel=“stylesheet” href=“/css/blueprint/print.css” type=“text/css” media=“print”>
    <!–[if IE]><link rel=“stylesheet” href=“/css/blueprint/ie.css” type=“text/css” media=“screen, projection”><![endif]–>
</head>
<body>
   
    <div class=“container”>

        <div class=“block”>
            <div id=“zfHeader” class=“column span-24″>
                <h1>Lorem Ipsum</h1>
            </div>
        </div>

        <div class=“block”>
            <div id=“zfExtraLeft” class=“column span-5″>
                <!– We‘ll add the style to style.css later –>
                <div class=”zfMenuLeft” style=”margin-top: 3em;”>
                    Lorem ipsum dolor sit amet
                    Lorem ipsum dolor sit amet
                    Lorem ipsum dolor sit amet
                    Lorem ipsum dolor sit amet
                </div>
            </div>
           
            <div id=”zfContent” class=”column span-18″>
               
                <?php echo $this->layout()->content ?>

            </div>

        </div>

        <div class=”block”>
            <div id=”zfFooter” class=”span-24″>
                <p>Copyright &copy; 2008 Pádraic Brady</p>
            </div>
        </div>

    </div>

</body>
</html>

In order to get the Admin Module using this layout, we need to intercept the Zend_Layout object just before an application request is dispatched to any Controller. We can then alter the default configuration we have used in our Bootstrap file. Welcome to the world of the Front Controller Plugin!

A front controller plugin already exists called Zend_Layout_Controller_Plugin_Layout which contains a postDispatch() method which is where the Layout gets finally rendered. To switch Layouts, we can simply create a new class extending the existing plugin, and add a preDispatch() method to detect when the Admin Module has been requested and replace the Zend_Layout layout name and the path to the Module’s layouts directory.

Front Controllers are very useful in this fashion for performing actions which have a dependency on the Module, Controller or Actions being requested. In Part 6 of this series, we’ll use another Front Controller Plugin to implement an Access Control List system for Authorisation using Zend_Acl which uses Module/Controller/Action names to check if the current user is authorised to access them.

To start, create a new directory tree within /library called ZFBlog. It will use the standard PEAR Convention directory structure for a class called ZFBlog_Layout_Controller_Plugin_Layout:

I suggest keeping an eye on ZFBlog. As you start to develop Zend Framework applications you will likely end up with two distinct types of additional classes. Zend Framework specific extensions and subclasses, and application specific classes. A lot of the time, such quirky additions can be reused in other applications. I’ve used this Layout plugin more than once ;-).

The contents of the Layout.php file could be as simple as:

<?php

class ZFBlog_Layout_Controller_Plugin_Layout extends Zend_Layout_Controller_Plugin_Layout
{
   
    public function preDispatch(Zend_Controller_Request_Abstract $request)
    {
        switch ($request->getModuleName())
        {
            case ‘admin’:
                $this->_moduleChange(‘admin’);
        }
    }

    protected function _moduleChange($moduleName)
    {
        $this->getLayout()->setLayoutPath(
            dirname(dirname(
                $this->getLayout()->getLayoutPath()
            ))
            . DIRECTORY_SEPARATOR . $moduleName . ‘/views/layouts’
        );
        $this->getLayout()->setLayout($moduleName);
    }
   
}

The above plugin is really simple. Before a request is dispatched we get the Module name from the Request object and check it against our switch statement. If a match is found, we can reset the Zend_Layout layout name and path for that request, or if nothing matches we leave things as they stand.

If you are particularly astute at refactoring, you may feel explicitly mentioning “admin” in the class is a bad move – I’ll leave it to readers to search for more flexible solutions handling multiple modules with different layouts.

Now, we have a new layout template and we have a plugin to utilise it when the Admin module is requested. Let’s make sure this new class replaces the Zend_Layout_Controller_Plugin_Layout class referenced by default in Zend_Layout. Guess where that gets done?

The Bootstrap! :-)
Check out the revised setupView() method where we change the default plugin class. Note that since this project is utilising Autoloading, and we’re storing the new plugin class in /library while also applying the PEAR Naming Convention, we require no other class inclusions and such. It just gets autoloaded when needed.

Note: The Zend Frameworks default autoloading function is only useful for classes and libraries strictly following the PEAR Conventions. I suggest moving non-PEAR Convention libraries and classes to a separate vendor directory parallel to library.

<?php

require_once ‘Zend/Loader.php’;

class Bootstrap
{

    public static $frontController = null;

    public static $root = ;

    public static $registry = null;
   
    public static function run()
    {
        self::prepare();
        $response = self::$frontController->dispatch();
        self::sendResponse($response);
    }
   
    public static function setupEnvironment()
    {
        error_reporting(E_ALL|E_STRICT);
        ini_set(‘display_errors’, true);
        date_default_timezone_set(‘Europe/London’);
        self::$root = dirname(dirname(<u>_FILE_</u>));
    }
   
    public static function prepare()
    {
        self::setupEnvironment();
        Zend_Loader::registerAutoload();
        self::setupRegistry();
        self::setupConfiguration();
        self::setupFrontController();
        self::setupView();
        self::setupDatabase();
    }
   
    public static function setupFrontController()
    {
        self::$frontController = Zend_Controller_Front::getInstance();
        self::$frontController->throwExceptions();
        self::$frontController->returnResponse(true);
        self::$frontController->setControllerDirectory(
            array(
                ‘default’ => self::$root . ‘/application/controllers’,
                ‘admin’ => self::$root . ‘/application/admin/controllers’
            )
        );
        self::$frontController->setParam(‘registry’, self::$registry);
    }
   
    public static function setupView()
    {
        $view = new Zend_View;
        $view->setEncoding(‘UTF-8′);
        $viewRenderer = new Zend_Controller_Action_Helper_ViewRenderer($view);
        Zend_Controller_Action_HelperBroker::addHelper($viewRenderer);
        Zend_Layout::startMvc(
            array(
                ‘layoutPath’ => self::$root . ‘/application/views/layouts’,
                ‘layout’ => ‘common’,
                ‘pluginClass’ => ‘ZFBlog_Layout_Controller_Plugin_Layout’
            )
        );
    }
   
    public static function sendResponse(Zend_Controller_Response_Http $response)
    {
        $response->setHeader(‘Content-Type’, ‘text/html; charset=UTF-8′, true);
        $response->sendResponse();
    }

    public static function setupRegistry()
    {
        self::$registry = new Zend_Registry(array(), ArrayObject::ARRAY_AS_PROPS);
        Zend_Registry::setInstance(self::$registry);
    }

    public static function setupConfiguration()
    {
        $config = new Zend_Config_Ini(
            self::$root . ‘/config/config.ini’,
            ‘general’
        );
        self::$registry->configuration = $config;
    }

    public static function setupDatabase()
    {
        $config = self::$registry->configuration;
        $db = Zend_Db::factory($config->db->adapter, $config->db->toArray());
        $db->query(“SET NAMES ‘utf8′”);
        self::$registry->database = $db;
        Zend_Db_Table::setDefaultAdapter($db);
    }
   
}

Go ahead and reload http://zfblog/admin in a browser.

All Contents Copyrighted to Pádraic Brady.

About the author: insic

Subscribe in my RSS Feed for more updates on Web Design and Development related articles. Follow me on twitter or drop a message to my inbox.

  • http://www.gowers.cn gowers

    nice theme, I like it.

  • http://www.hamroawaaz.com Rahul

    I found you via yahoo search. This article was quite helpful for me, just what i needed. Thanks.

  • Pingback: Zend Framework Blog Application Tutorial - Part 6: Introduction to Zend_Form and Authentication with Zend_Auth at INSIC 2.0 Web Development & Design Blog

  • Erwin

    I got a new issue. I did al the stuff for the new Bootstrap, added the admin directory include als his subdirectoris and created a IndexController and the index.phtml file for the admin.

    When i visit the url of my site i now get (ont the normal domain and the domain.com/admin url):
    Fatal error: Uncaught exception ‘Zend_Controller_Dispatcher_Exception’ with message ‘Invalid controller specified (error)’ in /home/domain/domains/domain.com/public_html/library/Zend/Controller/Dispatcher/Standard.php:249

    I dont get it because we didnt create a error handler yet but I still get the error, and I dont now what i did wrong.

  • Erwin

    What did i learn: always check the uppercase of you file names :).

  • http://bac.chaukhe.com Nguyen Duc Manh

    To: Erwin:

    Fatal error: Uncaught exception ‘Zend_Controller_Dispatcher_Exception’ with message ‘Invalid controller specified (error)’ in /home/domain/domains/domain.com/public_html/library/Zend/Controller/Dispatcher/Standard.php:249

    I think some folders or files wrong. Check it again.

    Eg:repair “view” folder to “views” or “controller” to “controllers” …etc

    http://bac.chaukhe.com

  • kapten_lufi

    that example used PDO_MYSQL…
    how if I use firebird database with PDO_firebird?
    can U help me??

    can u give me your contact like IM or email??

  • ricky

    Hi, can you please also post for version 1.11 as the above stuff gives many errors with that version…

    Thanks