Zend Books
— print (last updated: Sep 16, 2009) print

Select font size:
You can either install the project from existing sources or create it from scratch. If use the sources, download the ZendBooks.zip archive, extract it into the desired location and modify to suit your installation:
public/.htaccess     reset RewriteBase if necessary
public/index.php     replace '/usr/local/share/ZendLibrary' if necessary,
To create the application from scratch, run MakeZFProject and give it the Project Name of ZendBooks.

This is a full CRUD application with only minimal JavaScript usage.

Database setup

This application assumes the common MySQL database setup for testing:
host: localhost
dbname: test
username: guest
password: <empty>
The test database holds the books table with the usual content. Creating this is a matter of executing the following:
mysql -u root
mysql> create database if not exists test;
mysql> create user guest@localhost;
mysql> grant all on test.* to guest@localhost;
If the "create user" statement fails, it means the guest user already exists; just go on to the last statement.

Create/initialize the books table

The easiest way to create the books table is to open a terminal shell, navigate to the ZendBooks folder and execute the mysql command-line client running the table-creation script:
mysql -u guest test < table.sql
The table is created from scratch and populated with initial content.

The table.sql script of can be seen here: ( click to show )

Layout

We have modified the layout used in the ZendHello application from the Zend Framework Intro document.

application/views/layouts/layout.phtml
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.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" /> <title><?= $this->escape($this->pageTitle) ?></title> <?= $this->headLink() ?> <?= $this->headScript() ?> </head> <body> <h2 class="apptitle">Zend Books</h2> <?= $this->nav() ?> <div id="content"> <?= $this->layout()->content ?> </div> </body> </html>
by introducing the static header:
<h2 class="apptitle">Zend Books</h2>
and the non-static navigational content:
<?= $this->nav() ?>
We expect the navigational content to differ at different pages in the application and so the function nav is created and made available at the view level. In Zend terms, nav is a view helper function and defined in the following class:

application/views/helpers/Nav.php
<?php class Zend_View_Helper_Nav { public function nav() { return $this->view->render("nav.phtml"); } public $view; public function setView(Zend_View_Interface $view) { $this->view = $view; } }
Zend makes the connection from:
the nav function, assumed to be a view helper
to the path to the class (relative to application)
views/helpers/Nav.php
and within this file, the class name:
class Zend_View_Helper_Nav
and finally to the desired function:
public function nav() {
  return $this->view->render("nav.phtml");
}
When Zend instantiates the Zend_View_Helper_Nav class, the function setView is automatically called with the current $view object passed to it. The job of setView is to make $view available to the nav function.

The nav function renders the script nav.phtml, which is assumed to be in views/scripts (relative to application)

application/views/scripts/nav.phtml
<? if (!isset($this->navMenu)) return ?> <table class="nav"><tr> <? foreach($this->navMenu as $entry): ?> <td> <a href="<?= $this->url($entry['url']) ?>"><?=$this->escape($entry['label'])?></a> </td> <? endforeach ?> </tr></table>
The nav.phtml script generates a table of navigational links defined by the array variable:
$this->navMenu = array(
  array( "url" => action_controller_array1, "label" => label1 ),
  array( "url" => action_controller_array2, "label" => label2 ),
  ...
); 
Thus, the way a controller specifies what navigational menu to use is by setting:
$this->view->navMenu = ...
The nav.phtml script uses the built-in view function url in the expression:
<?= $this->url($entry['url']) ?>
in order to convert an action_controller_array into actual URL.

Cart creation

This application features a common notion of creating of a "shopping cart" of books. In this simplistic situation, the actual cart is treated as an array which maps book id numbers to quantityies. The important idea is that this cart must belong to some persistent storage. The simplest solution is to store the cart in the current browser session so that this information is not lost — at least as long as the browser session is active.

Zend regards sessions in this way:
$cartSpace = new Zend_Session_Namespace('ZendBooks-Cart');
corresponds roughly to the usual Php notation:
$_SESSION['ZendBooks-Cart']
The difference is that $cartSpace is an object and we access and/or create members in this object via:
$cartSpace->cart = /* this is the actual cart */
Out treatement of $cartSpace->cart is to regard this as a map from id's to quantities, e.g.,
$cartSpace->cart[$id] = $qty
Again, behind the scenes, this corresponds to
$_SESSION['ZendBooks-Cart']['cart']
Thus Zend's $cartSpace object provides a useful way of distinguishing portions of the session space.

Model

One of the key points to illustrate is the ease in which the "Books" class is constructed and used. The Zend_Db_Table class does all the work of constructing the ORM. We are implicitly using the Php PDO API as specified in the configuration file.

application/config.ini
[general] db.adapter = PDO_MYSQL db.params.host = localhost db.params.dbname = test db.params.username = guest db.params.password = date_default_timezone = "America/New_York"

application/models/Books.php
<?php class Books extends Zend_Db_Table { protected $_name = 'books'; }
The controller creates an instance of the books table:
$books = new Books();
and then uses the table representation in this way:

Controller

The controller generates behaviors for the following action sequences:
index: list available books
  |
  |==> show: show a certain book
  |      |
  |      |==> addtocart, redirect to showcart
  |
  |==> modify: show a selected book for modification
  |      |
  |      |==> update: update its values, redirect to index
  |
  |==> del: delete a book, redirect to index
  
create: generate a form for entering new book information
  |
  | => add: add it, redirect to index
  
showcart: show current cart contents
  |
  | => clearcart: clear it, redirect to showcart

IndexController code


application/controllers/IndexController.php
<?php class IndexController extends Zend_Controller_Action { private $listing = array('label'=>'Listing', 'url' => array('action'=>'index')); private $create = array('label'=>'Create', 'url' => array('action'=>'create')); private $showcart = array('label'=>"Cart", 'url'=>array('action'=>'showcart')); public function init() { $this->baseUrl = $this->getRequest()->getBaseUrl(); $this->view->navMenu = array($this->listing, $this->create, $this->showcart); $this->view->headLink()->appendStylesheet($this->baseUrl . "/css/main.css"); include_once "Books.php"; $this->cartSpace = new Zend_Session_Namespace('ZendBooks-Cart'); } public function indexAction() { $this->view->pageTitle = "Listing"; $books = new Books(); $allbooks = $books->fetchAll( null, array("id asc") ); $this->view->allbooks = $allbooks; $this->view->headScript()->appendFile($this->baseUrl . "/js/del.js"); } public function showAction() { $this->view->pageTitle = "Select Book"; $id = (int) $this->getRequest()->getParam('id'); $books = new Books(); $this->view->book = $books->fetchRow("id=$id"); } public function showcartAction() { $this->view->pageTitle = "Current Cart"; $this->view->cart = $this->cartSpace->cart; } public function addcartAction() { $id = (int) $this->getRequest()->getParam('id'); ++$this->cartSpace->cart[$id]; $this->_helper->redirector('showcart'); } public function clearcartAction() { unset($this->cartSpace->cart); $this->_helper->redirector('showcart'); } public function modifyAction() { $this->view->pageTitle = "Modify"; $id = (int) $this->getRequest()->getParam('id'); $books = new Books(); $this->view->book = $books->fetchRow("id=$id"); } public function updateAction() { try { $req = $this->getRequest(); $books = new Books(); $id = (int) $req->getParam('id'); $book = $books->fetchRow("id=$id"); $book->title = trim($req->getParam('title')); $book->type = $req->getParam('type'); $book->qty = trim($req->getParam('qty')); $this->check($book); $book->save(); $this->_helper->redirector('index'); } catch(Exception $x) { $this->view->info = $x->getMessage(); $this->view->book = $book; $this->view->pageTitle = "Display/Update: try again"; $this->render("show"); } } private function check($book) { if ($book->title == "") { throw new Exception("empty title"); } if (!preg_match("/^\d+$/", $book->qty) ) { throw new Exception("incorrect quantity format"); } } public function createAction() { $this->view->pageTitle = "Create"; } public function addAction() { try { $req = $this->getRequest(); $books = new Books(); $book = $books->createRow(); $book->title = trim($req->getParam('title')); $book->type = $req->getParam('type'); $book->qty = trim($req->getParam('qty')); $this->check($book); $book->save(); $this->_helper->redirector('index'); } catch(Exception $x) { $this->view->book = $book; $this->view->info = $x->getMessage(); $this->_forward("create"); } } public function delAction() { $id = (int) $this->getRequest()->getParam('id'); $books = new Books(); $books->delete("id=$id"); $this->_helper->redirector('index'); } }

Views

These initial view is:

application/views/scripts/index/index.phtml
<h2><?= $this->pageTitle ?></h2> <table class="display" cellpadding="5"> <tr> <th>id</th><th>title</th> </tr> <? foreach($this->allbooks as $book): $id = $book->id; ?> <tr> <td><?= $book->id ?></td> <td><?= $this->escape($book->title) ?></td> <td> <? $href = $this->url(array('action'=>'show', 'id'=>$id)); ?> <a href="<?= $href ?>">show</a> </td> <td> <? $href = $this->url(array('action'=>'modify', 'id'=>$id)); ?> <a href="<?= $href ?>">modify</a> </td> <td> <form action="<?=$this->url(array('action'=>'del'))?>"> <input type="hidden" name="id" value="<?=$id?>" /> <button onclick="del(this.parentNode)">delete</button> </form> </td> </tr> <? endforeach ?> </table>
The delete operation protected by the JavaScript del function defined in:

public/js/del.js
function del(theForm) { if (confirm("Are you really, really sure ?")) { theForm.submit() } }
The remaining views are these:

application/views/scripts/index/show.phtml
<h2><?= $this->pageTitle ?></h2> <input type="hidden" name="id" value="<?= $this->book->id ?>" /> <table cellpadding="5"> <tr> <td>id:</td> <td><?= $this->book->id ?></td> </tr> <tr> <td>title:</td> <td><?= $this->escape($this->book->title) ?></td> </tr> <tr> <td>type:</td> <td><?= $this->book->type ?></td> </tr> <tr> <td>qty:</td> <td><?=$this->book->qty?></td> </tr> <tr> <td></td> <td> <form action="<?=$this->url(array('action'=>'addcart'))?>" id="cart"> <input type="hidden" name="id" value="<?= $book->id ?>" /> <button type="submit">add to cart</button> </form> </td> </tr> </table>

application/views/scripts/index/showcart.phtml
<h2><?= $this->pageTitle ?></h2> <? if (!isset($this->cart) || !count($this->cart)): echo "Empty"; else: print_r($this->cart); ?> <p> <a href="<?=$this->url(array('action'=>'clearcart'))?>">Clear Cart</a> </p> <? endif ?>

application/views/scripts/index/create.phtml
<h2><?= $this->pageTitle ?></h2> <h4 class="error"><?= $this->info ?></h4> <form method="post" action="<?=$this->url( array("action" => "add") ) ?>"> <table cellpadding="5"> <tr> <td>title</td> <td> <input type="text" size="50" name="title" value="<?=$this->book->title?>" /> </td> </tr> <tr> <td>type</td> <td> <? $type = $this->book->type; if (!$type) $type = "paper"; ?> <? foreach(array("paper","cloth") as $t): ?> <? $selected = $t == $type ? "checked" : ""; ?> <input type="radio" name="type" value="<?=$t?>" <?=$selected?> /> <?=$t?> <? endforeach ?> </td> </tr> <tr> <td>qty</td> <td> <input type="text" name="qty" value="<?=$this->book->qty?>" /> </td> </tr> <tr> <td></td> <td><input type="submit" name="button" value="Add" /></td> </tr> </table> </form>

application/views/scripts/index/modify.phtml
<h2><?= $this->pageTitle ?></h2> <h4 class="error"><?= $this->info ?></h4> <form method="post" action="<?=$this->url(array('action'=>'update'))?>"> <input type="hidden" name="id" value="<?= $this->book->id ?>" /> <table cellpadding="5"> <tr> <td>id</td> <td><?= $this->book->id ?></td> </tr> <tr> <td>title</td> <td> <input type="text" name="title" size="50" value="<?= $this->escape($this->book->title) ?>" /> </td> </tr> <tr> <td>type</td> <td> <? $type = $this->book->type; ?> <? foreach(array("paper","cloth") as $t): ?> <? $selected = $t == $type ? "checked" : ""; ?> <input type="radio" name="type" value="<?=$t?>" <?=$selected?> /> <?=$t?> <? endforeach ?> </td> </tr> <tr> <td>qty</td> <td><input type="text" name="qty" value="<?=$this->book->qty?>" /></td> </tr> <tr> <td></td> <td><input type="submit" name="button" value="Update" /></td> </tr> </table> </form>

Additional Zend Features

Query Parameters

We use code like this to retrieve http query parameters.
$req = $this->getRequest();
$__ = (int) $req->getParam('__');
We could just as well use the standard $_GET and $_POST arrays, however this has the advantages of being neutral to the query method and also of better matching the Zend programming style

Forward, render, redirect

Forwarding means to invoke another script transparently to the browser request. We do this in one situation, in the add action in case of error:
$this->_forward("create");
What takes place is that create calls add, but add simply re-calls create while "add" remains as the visible URL.

Closely related to forwarding is rendering. We do this in the update action in case of failure:
$this->render("show");
In this case it means to go right to the show.phtml script without going through the entire show action. This is what we want because we want the changes the user is trying to make (which are in error) to be in effect.

Redirection means that the browser calls another action by sending a new request. This is done in add, update and delete on success:
$this->_helper->redirector('index');
sending the browser back to the book display list with the changes in effect.

Forwarding and rendering are meant for "internal" use only within the application, whereas redirection can redirect the browser to a whole new application on a different site. Zend has a simpler "_redirect" function used like the "_forward" function above, but it is intended for a more general usage

Style and JavaScript file inclusion

A new feature of this application is that we bring in a single JavaScript file by this call in the indexAction:
$this->view->headScript()->appendFile($baseUrl . '/scripts/del.js');
The javascript function simply offers a popup confirmation choice. Similarly, this statement in init
$this->view->headLink()->appendStylesheet($this->baseUrl . "/css/main.css");
puts the following stylesheet into effect for all actions:

public/css/main.css
body { padding: 0 10px 10px 10px; background-color: #eef8ff; } h2.apptitle { text-align: center; } table.display th { text-align: left; } table.nav td { padding: 0px 20px 0px 0px; } table.nav a { text-decoration: underline; } h4.error { color: red; } a { text-decoration: none; } a:hover { text-decoration: underline; color: red; }


© Robert M. Kline