Appendix C. Tips and tricks – Zend Framework in Action

Appendix C. Tips and tricks

 

The appendix covers

  • Using modules with Zend Framework’s MVC components
  • Understanding the effects of the case sensitivity of controller classes and action functions
  • Zend Framework URL routing components
  • Understanding your application using Zend_Debug, Zend_Log, and Zend_Db_Profiler

 

This is the where we think about the more advanced uses of some key Zend Framework components. Within the MVC components, we’ll look at how modules can be used to further separate your code and also investigate how the dispatcher handles actions and controllers with uppercase letters. We’ll also look at how the static and regex routing classes can provide more flexible and efficient URLs.

As a web application grows, logging the processes taking place becomes important. Zend_Log provides the features required to let your application tell you what’s going on. We’ll also look at the benefits of Zend_Debug for when you need a quick and dirty check while debugging your code.

Finally, we’ll look at Zend_Db_Profiler, which will show you exactly which SQL statements are run and how long each one takes. This is very useful when trying to make your page load as quickly as possible.

Let’s dive in and look at modules in the MVC system.

C.1 MVC Tips and Tricks

Within this section, we’ll look at features of the MVC system that we’ve not addressed within the rest of the book either because they’re more advanced or less likely to be used in most projects.

C.1.1 Modules

If you peruse the online documentation about Zend_Controller, you’ll notice that Zend Framework’s MVC system supports modules. Modules are designed to provide a further level of separation for your application and are usually used to separate different sections of your application to promote reuse of separate MVC mini-applications.

A simple CMS application may use two modules, blog and pages, each with its own directories for controllers, models, and views. Figure C.1 shows how the directories in this CMS application would look.

Figure C.1. A directory structure for a modular Zend Framework application may have its own set of MVC subdirectories.

Each module has its own controllers directory containing the controller classes. The classes use the naming convention {Module name}_{Controller name}Controller, and they’re stored in the file application/{Module name}/controllers/{Controller name}.php. For example, the index controller for the pages modules is stored in application/pages/controllers/IndexController.php and is called Pages_IndexController.

There is also the standard set of MVC directories in the application folder itself. These are the default modules and behave exactly the same as for a non-modularized application. That is, the controllers in the default module aren’t prefixed with the module name, so the index controller in the default module is simply named Index-Controller and is stored in application/controllers/IndexController.php.

By default, once the additional modules are registered with the front controller, the standard router will use the URL scheme of {base url}/{module name}/{controller name}/{action name}. For example, http://example.com/pages/index/view will execute the view action within the default controller of the pages module (the view-Action() method within the Pages_IndexController class).

Let’s look at how to register modules with the front controller, using the Places bootstrap code. Listing 3.1 in Chapter 3 introduced the Bootstrap class and set up the controllers’ directory, as shown in listing C.1.

Listing C.1. Front controller setup for Places in chapter 3

The important line is the call to setControllerDirectory() , where we tell the front controller where to find our controller classes. To include the additional ones in the modules, we can call the front controller’s addControllerDirectory() for each module in turn like this:

  $frontController->addControllerDirectory(ROOT_DIR . '/application/modules/blog, 'blog');
  $frontController->addControllerDirectory(ROOT_DIR . '/application/modules/pages, 'pages');

If you have many modules, this gets very longwinded. An alternative is to make use of addModuleDirectory(), which iterates over a directory and adds each subdirectory’s controllers directory for you. This is shown in listing C.2.

Listing C.2. Adding modules to the front controller setup for Places from Chapter 3

As you can see, to add support for all the controllers directories within the child folders of the modules directory, we need only a single line of code added to our bootstrap . Using addModuleDirectory() also solves a potential maintenance problem, because we never have to think about this code again. If we used addController-Directory(), we’d have to update this code every time a new module was added to the application.

In use, a modular application works the same way as a normal application. The url() view helper works with modules the same way. To create a URL to the index action of the index controller in another module, the code in the view script looks like this:

  $this->url(array('module'=>'another', controller=>'index',
             'action'=>'index'));

One issue that does come up is loading the module’s models. One way to do this is to dynamically alter the PHP include_path so the correct module’s models directory is on the path. This is best handled using a front controller plug-in, as shown in listing C.3. To avoid naming clashes, we use a class prefix to provide a namespace for the plug-in. The convention we will follow maps the class’s name to its location on disk. In this case, the class name is Places_Controller_Plugin_ModelDirSetup, so the class is stored in the file library/Places/Controller/ModelDirSetup.php.

Listing C.3. Using ModelDirSetup to set the include path to the module’s models

As with the other two plug-ins, ActionSetup and ViewSetup, the ModelDirSetup plugin is loaded in the Bootstrap class’s runApp() function using this code:

  $frontController->registerPlugin(newPlaces_Controller_Plugin_ModelDirSetup());

The class stores the current PHP include_path in the constructor , so that we can use it later. We do the actual work in the preDispatch() method. This allows us to set up the models directory for each action, which is important because it’s possible that each action in the dispatch loop will belong to a different module. We set the modules directory by calling dirname() on the controllers directory, which is obtained using the getControllerDirectory() method of the front controller and adding '/models' to it . We then prepend the models directory onto the previously stored include_path . This ensures that we don’t have multiple models directories on the include path when each action in the dispatch loop is executed.

With care, modules can be used to divide a complex application with many controllers into more manageable segments. If you need to access the models of another module, though, there is nothing to help you directly within Zend Framework, and often it’s easiest to use the full path in a require() statement.

Let’s look next at how the MVC system deals with capital letters in controller and action names.

C.1.2 Case Sensitivity

Zend Framework’s router and dispatcher are sensitive to the case of action and controller names. This is for a very good reason: PHP’s function and method names aren’t case sensitive, but filenames generally are. This means that there are rules when using capital letters or other word separators as controller names to ensure that the correct class files can be found. For action names, it’s important that the correct view script can be found. The following subsections outline the rules for controller and action URLs.

Word Separation within Controller URLs

The first letter of a controller class name must be uppercase—this is enforced by the dispatcher. The dispatcher also converts certain word-separator characters into directory separators when determining the filename for the class. Table C.1 shows the effect of different controller URLs on the filename and class name of the controller for a controller we wish to call “tech support”.

Table C.1. Mapping of controller URLs to controller class name and filename

Action URL

Controller class name

Controller filename

/techsupport/ TechsupportController TechsupportController.php
/techSupport/ TechsupportController TechsupportController.php
/tech-support/ TechSupportController TechSupportController.php
/tech.support/ TechSupportController TechSupportController.php
/tech_support/ Tech_SupportController Tech/SupportController.php

As you can see in table C.1, capital letters in controller names are always lowercased, and only period (.) and hyphen (-) word separators will result in a MixedCased class name and filename. In all cases, there is a direct mapping of class name to filename. This follows the standard Zend Framework conventions, so if an underscore is used in the controller’s URL, it acts as a directory separator on disk.

Action names have similar but slightly different rules, because both the dispatcher and the ViewRenderer are involved in resolving an action URL through to an action method and then on to the associated view script file.

Word Separation within Action URLs

The dispatcher enforces case sensitivity in action method names. This means that it will expect your action method name to be lowercase unless you separate the words in the URL with a recognized word separator. By default, only the period (.) and the hyphen (-) are recognized word separators.

This means that if you name your action using camelCase naming (such as view-Details), the action called will be the all lowercase viewdetailsAction(). The ViewRenderer will map both recognized word separators and also a capital letter change to a hyphen within the view script filename. This means that an action URL of view-Details will be mapped to a view script filename of view-details.phtml.

Table C.2 shows the effect of different URL word separators on the action and view script called.

Table C.2. Mapping of action URLs to action functions and view script filenames

Action URL

Action function

View script filename

/viewdetails viewdetailsAction() viewdetails.phtml
/viewDetails viewdetailsAction() view-details.phtml
/view_details viewdetailsAction() view-details.phtml
/view-details viewDetailsAction() view-details.phtml
/view.details viewDetailsAction() view-details.phtml

As you can see from table C.3, a certain amount of care needs to be taken when using word separators in URLs. We recommend that you always use a hyphen in your URL if you want to have word separation. This will result in easy-to-read URLs, camelCase action function names, and a hyphen separator in your view script filenames, which is consistent with the action name in the URL.

The rules for word separation in controller and action names follow a predictable system. In general, using the hyphen is easiest and also results in URL separations that Google and other search engines like, so we recommend you use them if you want to separate out multiword controller and action names.

Let’s look at how to use the router to our advantage for both speed and flexibility in converting URLs to action functions.

C.1.3 Routing

Routing is the process of converting a URL to an action to be run. The standard router is the rewrite router in the Zend_Controller_Router_Rewrite class, and by default it attaches the default route that a Zend Framework MVC application uses.

The default route translates URLs of the form module/controller/action/variable1/value1/. For example, a URL of news/index/view/id/6 will be mapped to the “view” action in the “index” controller of the “news” module with the additional parameter id set to 6. The id parameter is accessible in the controller using $this->_getParam('id').

Additional routes can be created and attached to the router, and these will also be used to map URLs to actions. Additional routes are usually set up to provide more easily comprehensible or shorter URLs. As an example, we could create a route that would allow the news/6 URL to map in exactly the same way as news/index/view/id/6. To do this, we’d need to create a route object and add it to the router in the bootstrap class before the front controller’s dispatch() method is called. Listing C.4 shows how this is done.

Listing C.4. Creating a route for news/{id number} in the bootstrap class

To set up a new route, we first have to create a Zend_Controller_Router_Route instance and set the route template (news/:id in this case ). The route template uses the colon character (:) as a prefix to indicate a variable placeholder name. Variable placeholders are then available within the request object and again are accessible from within the controller using the _getParam() member method. In this case, the id is set as a variable. If the route template doesn’t contain enough information to resolve a route to an action (that is, to provide the module, controller, and action names to be used), they must be specified in the $defaults array . Once the route is defined, it’s added to the router using the router’s addRoute() method .

Obviously, as many routes as are required can be added. The route object allows for validating the variable placeholders in the route to ensure that they’re of the required type, and it also allows for defaults, if the placeholder is missing. Listing C.5 shows the creation of a route that lists archived news entries for each year with URLs of the form news/archive/2008.

Listing C.5. Creating a route with defaults and requirements

Again, we set up the defaults for the module, controller, and action, but this time we also add a default for the :year placeholder. This means that if a value isn’t specified, it will be set to 2008 . The requirement for this route is that the :year placeholder must consist only of four digits. This is set using a regular expression in the $requirements array . If the :year placeholder doesn’t match the requirements, the route fails to match and the next route is tried. This means that a URL of news/archive/list wouldn’t match this route and would instead be matched to the list action of the archive controller in the news module.

Some routes don’t need the full flexibility of the standard route class. For instance, a route of /login, which maps to the login action of the auth controller, doesn’t need any variable placeholders. In these situations, Zend_Controller_Router_Route_Static is faster, because it doesn’t match variable placeholders. Its usage is similar to Zend_Controller_Router_Route, as shown in listing C.6, where we create a static route from the login/ URL to the login action of the auth controller in the default module.

Listing C.6. Creating a static route to /login

The creation of a static route is identical to a standard route except that you can’t use variable placeholders and so must define the module, controller, and action within the defaults array .

Alternatively, you may wish to have a URL that can’t be routed using the standard route because it’s too complex. In this situation, Zend_Controller_Router_Route_Regex can be used. This route is the most powerful and flexible, but also the most complex. It also has the added benefit of being slightly faster. Listing C.7 shows the new archive route expressed using the regex route.

Listing C.7. News archive route using Zend_Controller_Router_Route_Regex

Because the regex route doesn’t use placeholder variables, the resultant data is available within the request object using numeric keys starting at 1 for the first regular expression in the route. In the case of the route in listing C.7, the year value is extracted within the controller like this:

  $year = $this->_getParam('1');

This can complicate matters if the route is ever changed, because the numeric indexes may also change. Changes to routes are relatively rare, and judicious use of constants to hold the indexes would mitigate the cost of changes.

We’ve now covered some of the more advanced uses of the MVC system, so let’s move on and look at how to diagnose problems in your application.

C.2 Diagnostics with Zend_Log and Zend_Debug

Zend Framework provides the Zend_Log and Zend_Debug components to make it easier to find problems in your application’s logic. Zend_Debug is used more for transient checking of data and Zend_Log for longer-term logging of information about the application.

C.2.1 Zend_Debug

Zend_Debug is a simple class with one method, dump(), which either prints or returns information about a variable that is passed into it. It’s used like this:

  Zend_Debug::dump($var, 'title');

The first parameter is the variable you wish to display, and the second is a label or title for the data. Internally, the dump() method uses var_dump() and wraps the output in <pre> tags if it detects that the output stream is web-based. It will also escape the data if PHP isn’t being used in CLI mode.

Zend_Debug is best used for quick and dirty testing. If you want to put some more long-term diagnostics into your application, Zend_Log is a better fit.

C.2.2 Zend_Log

Zend_Log is designed to log data to multiple backends, such as files or database tables. Using Zend_Log to store log info to a file is very simple, as shown in listing C.8.

Listing C.8. Logging data with Zend_Log

The Zend_Log object needs a writer object to store the data. In this case, we create a stream writer to a file called /tmp/zf_log.txt and attach it to the Zend_Log object .

To store a message, the log() member function is used . When you store a message using log() you need to specify the priority of the message. The available priorities are listed in table C.3.

Table C.3. Priorities for Zend_Log log() messages

Name

Value

Usage

Zend_Log::EMERGE 0 Emergency: system is unusable
Zend_Log::ALERT 1 Alert: action must be taken immediately
Zend_Log::CRIT 2 Critical: critical conditions
Zend_Log::ERR 3 Error: error conditions
Zend_Log::WARN 4 Warning: warning conditions
Zend_Log::NOTICE 5 Notice: normal but significant conditions
Zend_Log::INFO 6 Informational: informational messages
Zend_Log::DEBUG 7 Debug: debug messages

The priorities are from the BSD syslog protocol and are listed in order of importance, with EMERGE messages being the most important. For each priority, there is a shortcut method named after the priority name that acts as an alias to log() with the correct priority set. This means that these two commands log exactly the same thing:

  $logger->log('Critical problem', Zend_Log::CRIT);
  $logger->crit('Critical problem');

These convenience methods mainly serve as shortcuts so that you type less code.

In a typical application, you’d create the $logger object at the start of the application, within the bootstrap, and store it to the registry for use in the rest of the application, like this:

  $writer = new Zend_Log_Writer_Stream(ROOT_DIR.'/tmp/log.txt');
  $logger = new Zend_Log($writer);
  Zend_Registry::set('logger', $logger);

When you create the Zend_Log object, you can choose which writer to use. There are four writers available, as shown in table C.4.

Table C.4. Zend_Log_Writer objects

Name

When to use

Zend_Log_Writer_Stream Stores logs to files or other streams. The 'php://output' stream can be used to display logs to the output buffer.
Zend_Log_Writer_Db Stores logs to database records. You need to map the level and message to two fields within a table.
Zend_Log_Writer_Firebug Sends log messages to the console in the Firebug extension to Firefox.
Zend_Log_Writer_Null Discards all the log messages. This can be useful for turning off logging during testing or for disabling logging.

To control the level of logging that is performed, a Zend_Log_Filter_Priority object is used. This sets the minimum priority of the message to be logged, and any messages of lower priority aren’t logged. The filter is attached to the log object using addFilter() as and when required. Usually, this is added at creation time, and the priority chosen is usually higher for a live site than for a test site. To limit logging to messages of CRIT or above, this code is used:

  $filter = new Zend_Log_Filter_Priority(Zend_Log::CRIT);
  $logger->addFilter($filter);

This means all information messages are discarded and only the very important ones are logged. For the live site, this will ensure that the performance of the application isn’t impeded by the time taken for logging.

We’ll now turn our attention to the profiler component within Zend_Db and see how we can display the SQL statements that are run.

C.3 Zend_Db_Profiler

Zend_Db_Profiler attaches to a Zend_Db adapter and enables us to see the SQL of queries that are run and how long each one took. We can use this information to target our optimization efforts, either by caching the results of long-running queries or by optimizing the query itself, maybe by tweaking table indexes.

If you’re configuring the database adapter using a config object, the easiest way to turn on the profiler is to set it within your INI or XML file. We use this mechanism for Places, and listing C.9 shows the Places config.ini with profiling enabled.

Listing C.9. Enabling database profiling within config.ini

All the data within the params section are passed to the Zend_Db database adapter, which then creates a Zend_Db_Profiler object and enables it.

To retrieve the profile information, the profiler’s getLastQueryProfile() method can be used. Listing C.10 shows how to log the query data from the fetchLatest() method of the Places model in application/models/Places.php within the Places application.

Listing C.10. Logging SQL query data within the fetchLatest() model function

First, we run the fetchAll() query and store the results to be returned at the end of the method. We retrieve the profile data for this query by getting an instance of the profiler and then calling getLastQueryProfile() . The query profile has some useful methods that we use to create a string to be logged . As we discussed in section C.2.2, we can retrieve the instance of the Zend_Log object, logger in this case, from the Zend_Registry, and log the message to the debug priority .

The resultant log entry looks like this:

  2008-02-02T17:00:00+00:00 DEBUG (7): Query: "SELEC  T `places`.* FROM
  `places` ORDER BY `date_created` DESC LIMIT 10", Params: , Time:
  0.70691108703613ms

In this case, there weren’t any bound parameters, so the Params section is empty. This is because the fetchAll() query simply orders the results by the date when they were created and limits them to the first time.

The profiler logs all events while it’s switched on, so the data for all queries can be extracted right at the end of processing and logged if required. In this case, you don’t need to alter any existing model methods and could just log the profile data after the call to dispatch() in the bootstrap. Listing C.11 shows an example of how to do this and assumes that the Zend_Log and Zend_Db objects have been stored to the registry.

Listing C.11. Logging all SQL profile data at the end of dispatching

After dispatch() has completed , we pick up the db and logger objects from the registry , then pick up the profiler from the database object. The profiler object has some methods that provide overall metrics, and we use getTotalElapsedSecs() and getTotalNumQueries() to provide a sense of how many database calls were made and how long all the database querying took .

The getQueryProfiles() method returns an array of Zend_Db_Profiler_Query objects, and we iterate over them, using the various member functions to create a single text string of information about each query within the $messages array . We format a single string containing all the information we wish to log and store it to the log at debug priority .

There’s quite a lot going on there, so it would be wise to factor it into its own function. The log produced looks something like this:

2008-04-06T20:04:58+01:00 DEBUG (7): 3 queries in 3.029 milliseconds
  Queries:
  0 - Query: "connect", Params: , Time: 0.603 ms
  1 - Query: "DESCRIBE `places`", Params: , Time: 1.895 ms
  2 - Query: "SELECT `places`.* FROM `places` ORDER BY `date_crea  ted` DESC
  LIMIT 10", Params: , Time: 0.531 ms

This information provides all we need to know about every query that took place in the generation of the page. In this case, we can see that the time taken to get the details of the places table using DESCRIBE was the largest, so we may choose to cache the database schema details using Zend_Db_Table_Abstract’s setDefaultMetadata-Cache() method.

C.4 Summary

In this appendix, we’ve looked at less commonly used features within Zend Framework. The MVC system is very flexible, and the modules system, in particular, allows for further separation of your code base if you need it. Routing allows you to provide your users with URLs that are easy on the eye and also good for search engines. The three provided routes provide plenty of options, but if they don’t meet your needs, the system is flexible enough to allow you to plug in your own router object or define your own routes to attach to the rewrite router.

While we all believe that we write bug-free code, it’s handy to have an easy way to inspect a variable or to log program flow to a file. Zend_Debug and Zend_Log provide opportunities to monitor what happens in your application when things go wrong and help you find problems. For database calls using Zend_Db, the built-in profiler can provide timing information along with the exact query that was executed. When integrated with Zend_Log, you have a powerful mechanism for finding database bottlenecks. This allows you to concentrate your optimization efforts on the queries where you’ll gain the most.