Wednesday, June 23, 2010

Action Helpers, Part 2: Logger

Now that we've seen how to use one of the existing Action Helpers in Part 1, let's look at building our own. This time, we'll build one for logging page views and the messages from the Flash Messenger.

As before, we'll start in Bootstrap.php by registering the helper:
protected function _initMsgLogger()
{
require_once('My/Action/Helper/MsgLogger.php');
Zend_Controller_Action_HelperBroker::addHelper(new My_Action_Helper_MsgLogger());

// make sure flash messager is also initialized if this is used
$this->_initMessenger();
}
Zend_Application contains a logging plugin that uses Zend_Logger and can be initialized through the application.ini file. If you look at ErrorController.php for the default application, you'll see that it references this Log resource. To initialize this resource for the application, add the following lines to application.ini (note: define BASE_PATH in index.php):
resources.log.stream.writerName = "Stream"
resources.log.stream.writerParams.stream = BASE_PATH "/data/logs/application.log"
resources.log.stream.writerParams.mode = "a"
resources.log.stream.filterName = "Priority"
resources.log.stream.filterParams.priority = 6
Note: Make sure that the application.log file has been created and is writable by the web user.

The priority filter will stop logging for messages above a certain priority. The built-in log priorities for Zend_Framework are DEBUG (7), INFO (6), NOTICE (5), WARN (4), CRIT (2), ALERT (1), and EMERG (0). So setting the priority filter to 6 will stop debug messages and allow everything else.

Now for the meat of the MsgLogger. Action Helpers have a couple of hooks where they can be inserted into the dispatch cycle, preDispatch() and postDispatch(). For our logger, we want to log any messages that were added to the FlashMessenger during the action, so we'll use the postDispatch() hook.
class My_Action_Helper_MsgLogger extends Zend_Controller_Action_Helper_Abstract {

public function getLog()
{
$bootstrap = $this->getActionController()->getInvokeArg('bootstrap');
if (!$bootstrap->hasPluginResource('Log')) {
return false;
}
$log = $bootstrap->getResource('Log');
return $log;
}

public function postDispatch()
{
if ($log = $this->getLog()) {
$request = $this->getRequest();
$log->debug($request->controller . ', ' . $request->action);

if ($flashMessenger = Zend_Controller_Action_HelperBroker::getExistingHelper('FlashMessenger')) {

$messages = $flashMessenger->getMessages()
+ $flashMessenger->getCurrentMessages();

foreach ($messages as $message) {
$log->info($message);
}
}
}
}
}
Note that even though we are getting our messages from the FlashMessenger here, they are still available in our layout as well. That is because there are no actions or redirects between our Action Helper postDispatch() and constructing the layout, which runs in a Controller Plugin postDispatch(). Here is that handy diagram by Kevin Schroeder again. If we had an Action Helper that was redirecting to another page in its postDispatch() method AFTER the MsgLogger was called, this could clear the messages before the layout can pick them up.

As is, this logger will log all the messages from the Flash Messenger as "info" and will additionally log the calling controller/action as "debug" (which means they will not be logged based on our current priority settings, but could be turned on for debugging). However, if you were using status codes on the FlashMessenger messages, then you could additionally log the message priority to the system log. We are using the INFO priority by calling $logger->info("msg"). However, we could use $logger->log("msg", Zend_Log::INFO) instead. So imagine we were using flashMessenger->addMessage(array(Zend_Log::ERR=>"error message")) when creating our messages. Then we could log something like this:
foreach $messages as $msgtype=>$message {
$info = $request->controller . ', ' . $request->action . ', ' . $message;
$logger->log($info, $msgtype);
}
Hopefully this gives some idea on how to create a custom action helper and some more insight into using the Zend_Logger for capturing application data.

Thursday, June 17, 2010

Action Helpers, Part 1: FlashMessenger

If you've done any reading on the Zend Forums, you may have noticed that extending the base controller class is frowned upon. The recommended solution is to use Action Helpers. If you haven't read Mathew Weier O'Phinney's definitive article on how action helpers work, you may want to do that first. I was still pretty confused after reading that article, but I'm starting to get it now.

This three part series will first cover the built-in FlashMessenger action helper, the recommended tool for passing messages to the user after page redirects. Next we'll set up application logging using Zend_Log through an action helper. Finally, I'll share a History action helper which stores the last 10 actions in a session variable and add a Back button to the site template which points to the calling page. Who knows, there may be more parts to this series later, because Action Helpers are just so incredibly useful.

Registered Action Helpers run preDispatch() after the controller init but before any action and postDispatch() after the controller action. Kevin Schroeder published a good simplified diagram of the Zend dispatch loop, if you need a visual reference. They can be set up application-wide by adding them to the bootstrap or specifically added to any controller in its init function. However you can also use action helpers on an as-needed basis, easily calling them from anywhere in the application.

I picked the FlashMessenger to start with because it is something I wish I'd known about when I started developing ZF applications. There is already an excellent article about the FlashMessenger by Noumenal over at A Fading Tan and I recommend you read it, but this is for people who like more complete examples. One time the FlashMessenger is very helpful is when a user enters data into a form and you want to redirect back to another page but indicate that the data was received. So let's set up a very simple example contoller with an index page and a comment page where the comment page contains a form. When the user submits the form, she is redirected back to the index page. If the comment field is empty, she will stay on the form page and get an error message both through the validator and the FlashMessenger. Here is some code:

/application/controllers/ExampleController.php:
<?php

class ExampleController extends Zend_Controller_Action {

public function indexAction() {}

public function commentAction() {
$form = new Zend_Form();

$comment = new Zend_Form_Element_Textarea('comment');

$form->addElement('textarea', 'comment');
$form->addElement('submit', 'submit');

if ($this->_request->isPost()) {
if ($comment->isValid($this->_request->comment)) {
$this->_redirect('/example/index');
}
}
$this->view->form = $form;
$this->view->form = $form;
}

}
/application/views/scripts/example/index.php:
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nunc mauris justo, faucibus vitae sagittis sed, laoreet rhoncus sapien. Suspendisse ac nunc vitae arcu tincidunt dictum. Mauris interdum, nisi ac convallis consequat, velit odio porttitor quam, at iaculis risus orci quis tellus. Nullam rutrum tempor mauris, id imperdiet velit semper sit amet. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Vivamus eleifend mollis cursus. Quisque et velit dolor. Sed nunc leo, euismod quis ultricies nec, gravida vitae purus. Nullam aliquet purus vitae urna tristique sed ultricies turpis condimentum. Fusce malesuada ipsum at ligula tristique et blandit nisi feugiat.</p>

<a href="http://www.blogger.com/example/comment">Submit a comment</a>
/application/views/scripts/example/comment.php:
<?php
echo $this->form;
?>
So now let's use the FlashMessenger to add a message in the commentAction and retrieve it in index.phtml. First we'll need to register the action helper, let's do this in Bootstrap.php to make it application-wide:
protected function _initMessenger()
{
require_once('Zend/Controller/Action/HelperBroker.php');
Zend_Controller_Action_HelperBroker::addHelper(new Zend_Controller_Action_Helper_FlashMessenger());
}
Now we'll add our messages to the ExampleController commentAction():
public function commentAction() {

(some code)

if ($this->_request->isPost()) {
$flashMessenger = $this->_helper->FlashMessenger;
if (!$comment->isValid($this->_request->comment)) {
$flashMessenger->addMessage('Please enter a comment.');
} else {
$flashMessenger->addMessage('Thank you for your comment.');
$this->_redirect('/example/index');
}
}
$this->view->form = $form;
}
As Noumanal points out, you do not really want to get the messages until you need them, which is in the view. We can retrieve the FlashMessenger Action Helper through the ActionBroker with the getExistingHelper() method. Notice that there are two methods that we call to get all the messages; getMessages gets the messages added before any redirect and getCurrentMessages gets any messages added without redirecting. We also need to call clearCurrentMessages() or else they will show up on the next request (the other messages are cleared automatically). For immediate gratification, add the following lines to the tops of your views, above the content:
<p><?php      
$flashMessenger = Zend_Controller_Action_HelperBroker::getExistingHelper('FlashMessenger');
$messages = $flashMessenger->getMessages()getMessages()
+ $flashMessenger->getCurrentMessages();
$flashMessenger->clearCurrentMessages();
echo '<p>' . join('<br>', $messages) . '</p>';
?></p>

(the page content)
Now of course you realize that this should really be in your layout.phtml file, right? See Layout and Navigation, Part 1 if you don't know how to set that up. Since that was so easy, let's do a brief example of how we would move this piece into a View Helper. Create a file libraries/My/View/Helper/FlashMessenger.php and basically just move the display code to here:
<?php

class My_View_Helper_FlashMessenger extends Zend_View_Helper_Abstract {
public function flashMessenger() {
$flashMessenger = Zend_Controller_Action_HelperBroker::getExistingHelper('FlashMessenger');
$messages = $flashMessenger->getMessages()
+ $flashMessenger->getCurrentMessages();
$flashMessenger->clearCurrentMessages();
echo '<p>' . join('<br>', $messages) . '</p>';
}
}
Add the path to your new View Helper to your Bootstrap.php:
protected function _initMessenger()
{
require_once('Zend/Controller/Action/HelperBroker.php');
Zend_Controller_Action_HelperBroker::addHelper(new Zend_Controller_Action_Helper_FlashMessenger());

// initialize the view helper
require_once('My/View/Helper/FlashMessenger.php');
$view = Zend_Layout::getMvcInstance()->getView();
$view->addHelperPath('My_View_Helper');
}
Now you can use this View Helper in your view scripts or layouts to echo your application messages:

<?php
echo $this->flashMessenger();
?>
I haven't looked at Noumenal's FlashMessenger View Helper, although it sounds pretty neat. One thing he mentions is that it uses key/value pairs for the messages which indicate a level such as notice, warning, etc, which seems like a good idea. This simple version should get you started, but keep in mind that you can use your View Helper to add many formatting enhancements that will then ensure a standard message format throughout your application.

Tuesday, June 15, 2010

Layout and Navigation, Part 3

Part 1 of this series showed how to modify the ZF quickstart application to use Zend_Layout for creating a site-wide template. Part 2 added Zend_Navigation for creating a left side menu and breadcrumbs. In this part, we'll have some fun by changing the default menu layout to use a jQuery plugin by Joel Birch called superfish (an enhanced version of suckerfish), a super sexy top navigation menu with dropdowns. Start by folling the preceeding link to the source and downloading the files, then copy the css, images, and js folders into your /public directory. We'll scrap the old style sheet and use the superfish style sheet instead, as well as including and calling the superfish scripts. We will need a very short custom stylesheet as well, which will be in styles.css. Let's go ahead and use the headScript and headLink view helpers as well. The HEAD of layout.phtml should look like this:
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>My Site</title>
<?php
// superfish styles
$this->headLink()
//->appendStylesheet('http://ajax.googleapis.com/ajax/libs/jqueryui/1.8.1/themes/base/jquery-ui.css')
->appendStylesheet('/css/superfish.css')
->appendStylesheet('/css/styles.css');

// jQuery and superfish scripts (hoverIntent is optional)
$this->headScript()
->appendFile('http://ajax.googleapis.com/ajax/libs/jquery/1.4.1/jquery.min.js')
->appendFile('/js/superfish.js')
->appendFile('/js/hoverIntent.js')
->appendScript("$(document).ready(function() { $('ul.sf-menu').superfish(); }");

echo $this->headLink();
echo $this->headScript();
?>
</head>
Superfish is actually very easy to hook up into Zend_Menu because everything keys off the top-level ul class. It turns out that there is a method for setting this, aptly named setUlClass(). So here is the BODY of layout.phtml:
<body>
<?php
// menu helper, use sf-menu as class for top-level ul element
echo $this->navigation()->menu()->setUlClass('sf-menu');
?>

<div class="content">
<?php
// fetch 'content' key using layout helper:
echo $this->layout()->content;
?>
</div>
</body>
Remember how I said we'll need another short stylesheet? That's because otherwise the page content appears to the right of the menu. No problem, just add the following lines to /public/css/styles.css:
div.content {
clear: left;
}
Super easy, no? And this gets us most of the way there. However, if you want those nifty little arrow indicators (and I'm sure you do) we'll have to go one step further and write a custom view helper. Because for any menu items with arrows, superfish wants the html to look like this:
<a href="#" class="sf-with-ul">menu item <span class="sf-sub-indicator"> »</span></a>
We'll create our custom View Helper in /library/My/View/Helper/Superfish.php. Start by specifying the path to your custom view helper in the Bootstrap.php _initNavigation() method, and go ahead and require the file:
//add custom helpers
require_once('My/View/Helper/Superfish.php');
$view->addHelperPath(
APPLICATION_ROOT . '/library/My/View/Helper/Navigation',
'My_View_Helper_'
);
We'll be extending Zend_View_Helper_Navigation_Menu, and the constructor looks like this:
class My_View_Helper_Superfish extends Zend_View_Helper_Navigation_Menu
{
public function superfish(Zend_Navigation_Container $container = null)
{
if (null !== $container) {
$this->setContainer($container);
}
return $this;
}
}
Because all the HTML markup changes are in the link rendering, the function we'll be overwriting is htmlify(). Basically we copy the htmlify function from Menu.php and then add a simple check for subpages, ending up with this:
public function htmlify(Zend_Navigation_Page $page)
{
(code from Menu.php)

// does page have subpages?
if ($page->count()) {
$sub_indicator = '<span class="sf-sub-indicator"> »</span>';
$attribs['class'] .= ' sf-with-ul';
} else {
$sub_indicator = '';
}

return '<' . $element . $this->_htmlAttribs($attribs) . '>'
. $this->view->escape($label)
. $sub_indicator
. '';
}
All you need to do to now use your superfish view helper is to call it in the layout.phtml instead of menu, like this:
echo $this->navigation()->superfish()->setUlClass('sf-menu');
Have fun playing around with the menus, adding new items, and reorganizing them to contain multiple levels, but as far as integrating superfish and Zend Framework is concerned, we're done. Happy coding!

Friday, June 11, 2010

Layout and Navigation, Part 2

In Part 1 of this series, I modified the ZF quickstart application to use Zend_Layout for an application-wide template. Part 2 will add a menu to the left sidebar of that template using Zend_Navigation.

Create a new file in the /application/configs directory called navigation.xml. Here is the content:
<?xml version="1.0" encoding="UTF-8"?>
<configdata>
<nav>
<content>
<label>Content Index</label>
<controller>content</controller>
<action>index</action>
<pages>
<page1>
<label>Page 1</label>
<controller>content</controller>
<action>page1</action>
</page1>
<page2>
<label>Page 2</label>
<controller>content</controller>
<action>page2</action>
</page2>
<page3>
<label>Page 3</label>
<controller>content</controller>
<action>page3</action>
</page3>
<page4>
<label>Page 4</label>
<controller>content</controller>
<action>page4</action>
</page4>
<page5>
<label>Page 5</label>
<controller>content</controller>
<action>page5</action>
</page5>
</pages>
</content>
</nav>
</configdata>
To use this XML file for creating the Zend_Menu requires adding an _initNavigation() function to the Bootstrap.php file. The Bootstrap file is a place to section out all the different application initialization methods. All of these are automatically run when $application->bootstrap()->run(); is called in index.php, or they can be selected and run individually (perhaps for other configurations such as web services or test) by calling $application->getBootstrap()->bootstrap('method_name'); at any time.

Here is the method you will want to add to initialize your navigation:
protected function _initNavigation()
{
// read in the xml menu
$xml = new Zend_Config_Xml(APPLICATION_PATH . '/configs/navigation.xml', 'nav');

// initialize the navigation object with the xml
$container = new Zend_Navigation($xml);

// Set the navigation object to the view
$view = Zend_Layout::getMvcInstance()->getView();
$view->navigation($container);
}
Now go back to your layout from Part 1 and replace the "menu goes here" text with this:
<?php
echo $this->navigation()->menu();
?>
One note here is that you should be developing in your development environment. If not, you will not get the errors and warning messages you may need to debug your menus. That is because the development section of application.ini includes these lines:
phpSettings.display_startup_errors = 1
phpSettings.display_errors = 1
resources.frontController.params.displayExceptions = 1
Now that the navigation is loading correctly and the menus are rendering, lets add a breadcrumb to the header just to show how easy this is. Replace the Header div in layout.phtml with the following:
<div id="Header">
MySite:
<?php
echo $this->navigation()->breadcrumbs()->setMinDepth(0)->render();
?>
</div>
That's all for now. Happy coding!

Thursday, June 10, 2010

Layout and Navigation, Part 1

This series of articles will cover a simple example of how to use Zend_Layout and Zend_Navigation for creating a 2-column template with a menu on the left side of the page. I'll be modifying the base application that gets installed by the quickstart. Let's get started.

The first thing we'll need is some content, and I'm going to use the simplest content possible for this example. So I'll create my ContentController.php and then add blank actions for an index plus 5 pages. The code looks like this:
class ContentController extends Zend_Controller_Action
{
public function indexAction() {}
public function page1Action() {}
public function page2Action() {}
public function page3Action() {}
public function page4Action() {}
public function page5Action() {}
}
And of course you need view scripts for each of these, set these up in /application/views/scripts/content and they'll be named page1.phtml, page2.phtml, etc. My content in each file is simply "This is page n." (where n is the page number) so that I can verify that the correct page is being pulled up. This should work immediately after creating the pages by going to your url, for example http://zendreflections/content/page1, so check this first.

Now that there's some rudimentary content, we'll add a simple layout to the site. Add Zend_Layout to the MVC by adding the following line to your index.php bootstrap file:
    /**
* Layout helper
*/
require_once 'Zend/Layout.php';
Zend_Layout::startMvc();
By default, Zend will look for a file named layout.phtml in the /application/views/scripts directory. For this example, let's put the layouts in a different directory, say /application/views/layouts. One way to do this is to add the following line to /application/configs/application.ini:
resources.layout.layoutPath = APPLICATION_PATH "/views/layouts"
So now we need a layout file (layout.phtml). Since this is a Zend article and not a css article, I'm going to grab my css from somewhere else, http://www.bluerobot.com/web/layouts/layout1.css (see how it looks here). This layout has Header, Content, and Menu divs. Here is the code:
<!DOCTYPE html
PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>My Site
<link href="http://www.bluerobot.com/web/layouts/layout1.css" rel="stylesheet" type="text/css">
</head>
<body>

<div id="Header">My Site</div>

<div id="Content">
<?php
// fetch 'content' key using layout helper:
echo $this->layout()->content;
?>
</div>

<div id="Menu">
menu goes here
</div>

</body>
You see there is currently a placeholder for the menu. That's because we'll have to create it first using Zend_Navigation, which will be the subject for the next article.

Happy coding!

Wednesday, June 2, 2010

Multiple Zend Framewok applications on Ubuntu

This article is only slightly Zendy, and a bit more about how to configure Ubuntu so that you can work on multiple websites at once for local development. It does include a few tips on how I think the default index.php file should be modified if you are using Zend Framework libraries for multiple sites, so even if you are using a different development environment you may still find that part useful. This does not cover setting up sites that the rest of the world can see, after they are ready I assume you will FTP them to another server or something. Since I am having to do this now in order to develop and test code for this blog, I figured now would be a good time to write about it.

There are a few files that you will need to worry about:
/ect/hosts (list of hostnames)
/etc/network/interfaces (for setting up multiple network IP addresses, reboot after changing)
/etc/apache2/httpd.conf (includes site-specific httpd.conf files to configure Virtual Hosts)

In /etc/hosts, you will first set the loopback IP addresses to point to your server. Then pick some IP's like 192.168.X.Y to use for your sites. Anything on 192.168 is using your private network, which is what you want here. There were a few "default" lines at the end of /etc/hosts; you can leave them in, they won't hurt anything. Here is an example of what /etc/hosts should look like:
127.0.0.1       your_computer_name
127.0.1.1 your_computer_name

192.168.13.1 mooneleaf
192.168.13.2 zendreflections

# The following lines are desirable for IPv6 capable hosts
::1 ip6-localhost ip6-loopback
fe00::0 ip6-localnet
ff00::0 ip6-mcastprefix
ff02::1 ip6-allnodes
ff02::2 ip6-allrouters
ff02::3 ip6-allhosts
In /etc/network/interfaces, you can now use tap to create a map to each IP address you have configured. Here is an example using the IP addresses selected above:
auto lo
iface lo inet loopback

auto tap0
iface tap0 inet manual
up ifconfig tap0 192.168.13.1 up
down ifconfig tap0 down
tunctl_user root

auto tap1
iface tap1 inet manual
up ifconfig tap1 192.168.13.2 up
down ifconfig tap1 down
tunctl_user root
In /etc/apache2/httpd.conf, you will just include in your site-specific httpd.conf files that will do the actual configuration of your Virtual Hosts. Here is an example:
Include /data/mooneleaf/httpd.conf
Include /data/zendreflections/httpd.conf
So let's say that you are starting a brand new Zend Framework project, as I am. In that case, you can use the Zend Framework command line tool to create your initial project files like this:
$ /data/lib/ZendFramework-10.2/bin/zf.sh create project zendreflections
Next create your project-specific httpd.conf file. I like to keep my Zend Framework libraries in a separate location so they can be referenced by all my various Zend Framework projects. Since I use subversion to manage my code, it also makes sense to keep the Zend libraries separate from my repository. I can keep multiple versions and then just reference the specific one I am using in my index.php, which makes it easy to keep up with which version is being used at any given time. Here is an example of httpd.conf for my zendreflections project:
<VirtualHost>
ServerName 127.0.0.1
DocumentRoot /data/zendreflections/public

<Directory>
AllowOverride All
Options All
</directory>

php_value magic_quotes_gcc off
php_value register_globals off

</VirtualHost>
You'll need a couple of modifications to the index.php file to include your Zend libraries. Since your Zend libraries may be in a different location for different application environments, add a switch statement to specify their location. I also like to use the ZendX libraries, so I'll include the path to "extras" as well. Here is what I think is a better format for the top half of index.php:
// Define the base path
defined('BASE_PATH')
|| define('BASE_PATH', realpath(dirname(dirname(__FILE__))));

// Define path to application directory
defined('APPLICATION_PATH')
|| define('APPLICATION_PATH', BASE_PATH . '/application');

// Define application environment
defined('APPLICATION_ENV')
|| define('APPLICATION_ENV', (getenv('APPLICATION_ENV') ? getenv('APPLICATION_ENV') : 'development'));

// Define Zend Framework version
defined('ZF_VERSION')
|| define('ZF_VERSION', '1.10.2');

// Keep switch statement updated when adding environments
switch (APPLICATION_ENV) {
case "development":
$zendDir = '/data/lib/Zend/ZendFramework-' . ZF_VERSION;
break;
}

// Ensure application, library, and ZF libraries are on include path
set_include_path(implode(PATH_SEPARATOR, array(
APPLICATION_PATH,
BASE_PATH . '/library',
$zendDir . '/library',
$zendDir . '/extras/library',
get_include_path(),
)));
Actually, I would recommend to put all this into a separate application.php file and just include it from your index.php. That way, if you want to set up another entry point to your application later (for web services, for example) all your setup code can be reused.

You will probably need to reboot for your changes to /etc/network/interfaces to take effect, but once you do you this and type "zendreflections" (or whatever label you chose for your IP address) into the URL, you should see the reassuring "Welcome to the Zend Framework" message and you can finally start to develop your awesome web application. Happy coding!