Zend Framework: Zend_Application Component Proposal
| Proposed Component Name | Zend_Application |
|---|---|
| Developer Notes | http://framework.zend.com/wiki/display/ZFDEV/Zend_Application |
| Proposers | Bill Karwin |
| Revision | 1.1 - 17 August 2006: Initial proposal posting. (wiki revision: 11) |
Table of Contents
- 1. Overview
- 2. References
- 3. Component Requirements, Constraints, and Acceptance Criteria
- 4. Dependencies on Other Framework Components
- 5. Theory of Operation
- Using Config Files
- Registering Autoloader
- Configuring Zend_Application
- Creating Application Resources
- 6. Milestones / Tasks
- 7. Class Index
- 8. Use Cases
- 9. Class Skeletons
1. Overview
This is a proposal for a general-purpose bootstrap class for Zend Framework MVC applications. The objectives are (1) to implement a reusable and extensible class for bootstrapping web-based applications, and (2) establish a convention for configuration-driven application instances.
2. References
3. Component Requirements, Constraints, and Acceptance Criteria
- Component will instantiate Zend_Controller_Front or user-specified front controller class.
- Component will load config data from an array, file, or Zend_Config object and use the properties to configure the front controller.
- Component will register the autoload callback of Zend_Loader or user-specified loader class.
- Component will register a front controller plugin to load module-specific configuration files and process them with Zend_Application_Resource or user-supplied subclass of that component.
- Component will support conventions for locations and names for config files, modules, controllers.
- Component will proxy a run() method to the front controller, to dispatch web application requests.
4. Dependencies on Other Framework Components
- Zend_Application_Resource
- Zend_Config
- Zend_Controller_Front
- Zend_Loader
- Zend_Registry
5. Theory of Operation
The Zend_Application class is a general-purpose, configurable bootstrap class. It configures an instances of Zend_Controller_Front, sets an autload callback, and dispatches to the front controller. The only parameter you are required to give to Zend_Application is the home directory of the application. Then use the run() method to start processing the request.
An example of the simplest usage of Zend_Application is the following:
The Zend_Application constructor processes the home directory argument with the realpath() PHP function. If this function determines that the path is invalid, a Zend_Application_Exception is thrown.
Using Config Files
Zend_Application attemps to read a "front" config upon startup. This config contains data specific to configuring the Front Controller and other resources that are application-wide, such as the Zend_Session.
Specify the config in the second constructor argument, or in the setConfig() method. You can specify the config in the following ways:
- Zend_Config object instance - you can load the config file yourself in your bootstrap script, and pass the object to Zend_Application.
- PHP array - you can declare a plain PHP array, so you don't have to write config data in a file at all.
- Path to the config file.
- Path to the directory containing the config file. The directory is searched for a file called 'config.ini' or 'config.xml'. The Zend_Config subtype is determined from the file extension. If more than one config.* file exists, it is non-deterministic which file is used, so it is best to specify the full filename. Only files named 'config.*' are searched; if you have a different filename, it is best to specify the full filename.
- If you specify nothing, or a PHP null, this means to search the application home directory according to the behavior in the previous case.
In addition to the "front" config file, each application module may have its own config file. This config file specifies module-level resources and other module-specific data. Also, routes can be defined in each module's config file. Zend_Application includes a front controller plugin that loads the appropriate config files for a ll modules and adds all routes prior to routing. Then after routing, during the pre-dispatch step, the plugin processes other entries in the module's config file, so that resources are loaded only for the module to which the request is routed.
By default, this config file is located in a directory "config" which is a sister to the "controllers", "modules", and "views" directories.
The name of the "config" directory can be customized with a property in the front config file called 'moduleConfigDirectoryName', but that directory name is still relative to a given module's home directory.
Both .ini and .xml formats are supported.
Registering Autoloader
By default, Zend_Application registers an autoload callback, using Zend_Loader::registerAutoload().
You can specify properties 'autoloadEnabled' and 'autoloadClass' in the front config, to override the behavior of whether or not to enable autoloading, and the class to use for the autoload callback.
Also, if you subclass Zend_Application, you can specify different default values in the protected class members $_autoloadEnabled and $_autoloadClass
Configuring Zend_Application
Zend_Application uses config properties to declare customizations to the front controller and other application bootstrapping. None of the config properties are mandatory.
The following config properties are recognized:
| Property | Description |
|---|---|
| 'applicationResourceClass' | String names the class for the resource plugin to use to read module config files. |
| 'autoloadEnabled' | Boolean; enable the autoload callback if true. Default true. |
| 'autoloadClass' | String names the class to use for the autoload callback. Default Zend_Loader. |
| 'baseUrl' | String names the base URL of the application. This is passed to the front controller's setBaseUrl() method. |
| 'configFile' | String specify the modules' config file name, to be found by the front controller resource plugin. |
| 'configSection' | String specifies the config section to be read in the modules' config files. |
| 'controllers' | Array or Zend_Config object, maps one or more module names to their respective controller directories, in the manner supported by the front controller's setControllerDirectory() method. |
| 'frontControllerClass' | String names the class to instantiate the front controller. The class must be loadable and extend Zend_Controller_Front. |
| 'modules' | If this is the Boolean true value, the application support modules, and the modules are assumed to exist under the application home directory. If this is a string, array, or Zend_Config object, it maps one or more module names to their respective module directories, in the manner supported by the front controller's addModuleDirectory() method. |
| 'moduleConfigDirectoryName' | String names the directory for config files, under each module directory. Default is "config". |
| 'moduleControllerDirectoryName' | String names the directory for controllers, under each module directory. Default is "controllers". |
| 'moduleModelDirectoryName' | String names the directory for models, under each module directory. Default is "models". |
| 'params' | Array or Zend_Config object, maps parameter names to values, in the manner supported by the front controller's setParams() method. |
| 'resourcePluginClass' | String names the class to instantiate for processing module config files. |
| 'returnResponse' | Boolean; this is passed to the front controller's returnResponse() method. |
| 'routes' | Zend_Config object, contains a heirarchy of properties to declare routing rules for the front controller, in the manner supported by the rewrite router's addConfig() method. |
| 'saveResourcesInRegistry' | Boolean; this is passed to the Zend_Application_Resource class. Default is true. |
| 'throwExceptions' | Boolean; this is passed the the front controller's throwExceptions() method. |
If any other customization of the front controller is needed, the developer can call the Zend_Application getFrontController() method to return the front controller object.
Creating Application Resources
Zend_Application includes a front controller plugin, called Zend_Application_Plugin. This plugin uses the proposed Zend_Application_Resource component. This component performs the following tasks prior to routing:
- Read each module's config file.
- Add all routes defined in the config files (this may supplement the routes defined in the front config file, or may be used instead of defining routes in the front config).
When a request is routed to a given module, additionally perform the following tasks:
- Store the config object loaded from the module config dir in the registry, under registry key 'config'.
- Instantiate resource classes declared in that config file.
- Store the resources in the registry.
If the current request is in a non-default module, the config object and the resources instantiated by Zend_Application_Resource are stored in a new Zend_Registry object, which is in turn stored in the registry. The key for the new registry is the name of the module. This is to avoid key conflicts if two modules use the same key name.
Thus if a module named "blog" loads resources based on the module's config file, code within the blog module can retrieve a resource from the registry-within-a-registry as follows:
6. Milestones / Tasks
Milestone 1: [DONE] Publish proposal.
Milestone 2: Revise proposal, approve for Incubator development.
Milestone 3: Commit working prototype to Incubator.
Milestone 4: Commit working unit tests.
Milestone 5: Write end-user documentation.
Milestone 6: Release prototype in incubator.
Milestone 7: Revise implementation, tests, and documentation based on feedback.
Milestone 8: Merge changes from Incubator to Core.
7. Class Index
- Zend_Application
- Zend_Application_Plugin
- Zend_Application_Exception
- Zend_Application_Resource_FrontController
8. Use Cases
| UC01: Bootstrap a simple web application |
|---|
| UC02: Specify a base URL for requests |
|---|
| UC03: Bootstrap script of web application with default module location |
|---|
| UC04: Adding a custom module location |
|---|
| UC05: Adding multiple custom module locations |
|---|
| UC06: Adding custom controller locations |
|---|
| UC07: Customizing the Front Controller |
|---|
| UC08: Declaring routes in an array |
|---|
| UC09: Declaring routes in a config file |
|---|
front-config.ini:
index.php:
9. Class Skeletons
See proof-of-concept code and unit tests in the Zend Framework Laboratory. Check it out using Subversion (http://framework.zend.com/svn/laboratory), or browse it online using FishEye (http://framework.zend.com/fisheye/browse/Zend_Framework_Laboratory/library/Zend).
This is difficult to make a generic error handler for exceptions encountered in the bootstrap process.
What if the exception is due to the MVC framework itself failing? How can you redirect to an error display if that occurs? In the bootstrap code, the only thing we can be certain is working is the web server's ability to run a PHP script.
So I would suggest we make a class Zend_Application_ErrorHandler, which does something like what you show above, but it also does its own exception catching, and redirects to config-specified static HTML page in case of catastrophe.
My bootstraps were getting bloated and repetitious. I had a go at creating an application class. I found it troublesome in practice when trying to accommodate different set ups like for example using some of the code in current proposals where special set ups can be needed.
So I re-wrote it as an entirely static helper class and that seems to be working out. It allows me to cut down on the bootstrap bloat while retaining flexibility.
My failure at an instantiated approach could have been just inadequate design. I just wanted to share my experience, and look forward to seeing how this proposal goes.
Here's the methods I'm using if it's of any interest (not pretty enough to show in full):
[CODE]
public static function customizePhpBehaviour($appDir = null, $configEnv = null)
public static function prependIncludePath($path)
public static function getAppDir()
public static function getAppDirAssumingSelfUnderPublicDir()
public static function getConfigEnv()
public static function isProductionServerDetected()
public static function customizePhpErrorReporting($configEnv)
public static function checkVersionRequirements($options)
public static function getAppName()
public static function boot($appDir = null, $configEnv = null)
public static function bootstrapZendFramework($appDir = null, $configEnv = null)
public static function runApp()
public static function initRegistry($appDir = null, $configEnv = 'production')
public static function setupController($request = null, $response = null)
public static function portablePath($path)
[/CODE]
Right – using an object-oriented approach can help with reducing bloat.
The proposal shows how we can achieve great flexibility and customization, using a mix of several programming mechanisms:
- Object-oriented extensibility (i.e. subclassing)
- Configuration specified in config files
- Configuration by method calls and parameter values
- Plugin architecture to enable extra functionality (see proposal for Zend_Application_Resource)
Would it be possible to include a method so that it could create a Zend_Cache_Page() object? Like so:
Either that or it would be great to allow an init() method (similar to the Controller, Db classes) for user-defined hooks that fire off after construction so that objects like this could be created within a sub-classed instance of Zend_Application.
Will the path parameters supplied to Zend_Application be converted to realpaths during processing? I find it a bit less verbose and more elegant to supply:-
'../application/controllers'
than
dirname(dirname(_FILE_) . '/application')
One last feature request would be to auto-discover an environment based on its server name or IP address. That way you can pass 'production' or 'development' to your Zend_Config_Ini file without needing to hardcode it into your bootstrap - handy if you're sharing a site in a number of different locations simultaneously. I've got the code for this sitting in the Zend_Environment component if you're interested.
Regarding Zend_Cache_Page:
You should take a look at the Zend_Application_Resource proposal. That's my intended solution to enable user-defined plugins for the application bootstrap process. So we would write a class that extends Zend_Application_Resource_Abstract, for example we could call it Zend_Application_Resource_Cache_Page.
The presence of this class would implicitly mean that a config file could contain properties with the config data to be passed to Zend_Application_Resource_Cache_Page. That class would contain the logic to map config data to the Zend_Cache factory.
Note the config data doesn't necessarily have to be passed verbatim to the Zend_Cache factory; the resource manager class gets to interpret the config data and do the right work to create the object.
Using this kind of plugin architecture, I hope we can avoid adding lots of special-case hooks to Zend_Application. The plugins would be loaded automatically on demand, based on the values found in the config files. Thus the base Zend_Application class would remain pretty lean.
Regarding realpath and using ".." instead of dirname():
Sure, we can call realpath on the home directory specified. That way the bootstrap could use relative paths easily. And if an absolute path were specified, that'd work too.
Regarding discovery of environment and corresponding config section:
We've been talking about this internally a bit. There are several ways to do this, with different advantages and disadvanteges.
1. Declare instance name in php.ini, retrieved with ini_get()
Advantage: portable, centralized.
Disadvantage: users often don't have access to edit php.ini. If you need to deploy many apps, this could create an explosion of php.ini directives.
2. Define PHP variable directly in bootstrap script
Advantage: portable, flexible, keeps config in pure PHP code instead of involving other types of environmental config.
Disadvantage: requires editing application bootstrap on each deployed instance. The app code should be under revision control, so the bootstrap file would show up as locally modified, and this is confusing.
3. Use superglobals $_SERVER['HTTP_HOST'] or $_SERVER['SERVER_NAME']
Advantage: mostly portable, keeps config away from bootstrap code.
Disadvantage: breaks if the hostname or virtual host name changes. Also if two hosts need the same config section, you have to create duplicate sections for no reason.
4. Use environment variable, set in Apache with the SetEnv directive, either in the global httpd.conf or in .htaccess or else set in system configuration and passed to web app with the Apache PassEnv directive.
Advantage: isolates instance variable from application code.
Disadvantage: specific to web server and deployment technology (Apache, mod_php, fastcgi, isapi, etc.). Also, the web server config should be under revision control as well.
5. Declare instance name in a one-line config file
Advantage: portable, good for users without access to php.ini.
Disadvantage: performance cost of reading an extra unnecessary file. Also, it doesn't implicitly change as you deploy to a different server, so one could accidentally deploy the wrong config.
Also, I am not sure about relative paths when using ZA with the cli as the paths are changed to where the current dir is.
It might be a good idea to append the home directory instead for modules and any other paths that are relative for the cli.
Another thing I thought about was setting the home dir as the default module directory. Wouldn't it be better if the default was something like $appHomeDir . 'modules' as a home directory does have other folders than module specific stuff.
I think 'autoloadEnabled' should not be enabled by default since autoloading prevents opcode caches from doing their work on autoloaded files.
This point about autoloading is a good point, so I consulted an expert who works on Zend Platform.
It is not true that autoloading is incompatible with opcode caching. It works fine. There are cases when a class cannot be cached, but these cases happen whether you use autoload or not.
The performance difference between using autoload vs. require_once is not significant for most applications. If your application is so sensitive the performance impact of using autoload matters, then it's much more likely that other system architecture changes will help to a much greater degree than micro-optimizing class loading.
The bottom line is that using autoload makes development easier, and this outweighs its potential disadvantages.
If you still want to set autoload to false, there are at least two alternatives to accomplish this:
- Specify the autoloadEnabled option to the constructor of the Zend_Application class:
- Create your own extended Zend_Application class and change the default:
| Prototype Code Moved I have removed the attached Zip file from this proposal page, and instead checked the proof-of-concept code into svn in the ZF Laboratory: Zend_Application code and tests are under library and tests. |
$dir . DIRECTORY_SEPARATOR . $file should be $path
Thanks, I'm just trying out the component in a simple test application. It's a step forward, but I think it still needs more hooks.
btw, the construct in Zend_Application_Plugin conflicts when used with modules because it tries to load a module config before the module directory is set and before the dispatch() process has started.
Can you describe examples of what type of hooks are needed?
Also keep in mind that you can always do $app->getFrontController() and then do less common types of customization. The Zend_Application interface, like most components in ZF, should provide a simple solution for the 80% most common usage, and then allow for extensibility for other more exotic cases.
I found a wee bug in the _doLoadConfig() method. Instead of
...it should be
...otherwise you never end up with a Zend_Config object.
Also, there's a test for the 'config' filename using the 'filename' property of pathinfo. Is it possible to use a strpos or something similar for all us non-5.2.0 users? ![]()
Finally I'm trying to put together the relationship between the Zend_Application and the Zend_Application_Resource family. Is it true to say that just by having the 'db' key set in my config that a Zend_Db object is auto-created or do I have to initiate that manually?
- You don't get an object for which get_class($config) == 'Zend_Config', but you do get an object that passes the test $config instanceof Zend_Config.
- Apologies for the pathinfo() usage. I will change it to work with PHP 5.1.4.
- Yes, the idea is that Zend_Application iterates through each module's config file and if it finds config properties that match a certain naming convention, and a corresponding wrapper class exists that extends Zend_Application_Resource_Abstract, then the config data is used to instantiate that resource.
Oh and another - the '_config' always seems to be null. I'm guessing that's because the Zend_Config object is always out of scope?
I don't understand what context you're referring to. The $_config object in Zend_Application is set in the constructor, or with the setConfig() method.
Indeed it is but in PHP 5.1.6 all I'm seeing is a null value in that property. I'm guessing there's a slightly revised handling of class/object contexts in PHP 5.2.0? I was testing my config file and sure enough it is being correctly read and parsed in the static _loadConfig methods, but when it is passed back to the setConfig() method it is a null value, so I've assumed that the Zend_Config object has gone out of scope. Try it in PHP 5.1.6 and you'll see what I mean.
Bill – This looks good. I have a few notes:
- Allow config.php
- This allows those of us who like using arrays for configs to do so
- Does application module config need to be in a separate directory?
- Seems like having it at the toplevel of the module would be sufficient
- Need configuration for:
- Plugins
- Should allow specifying either:
- Directory of plugins to load
- array of plugin => path values
- Should allow specifying either:
- Action Helpers
- Should allow specifying:
- prefix => path pairs for action helper paths
- class => path pairs of specific helpers
- should allow a second argument to load at start
- Should allow specifying:
- Plugins
- Should allow specifying:
- error controller to use (pass to error handler plugin)
- view class/object to use with ViewRenderer
- Eventually: whether or not to use layout, and layout config options
ZF Home Page
Code Browser
Wiki Dashboard
I've been working on something similar to this, but it's nice to see that zf will have an offical component to handle the bootstrap. Knowing bootstrap/configuration errors/exceptions can occur, I feel that some sort of errorHandler might be useful to either redirect to a static page etc.. At the moment some of us are doing something like this: