Skip to content


How to lazy load modules with Zend Application and Zend Framework

Overview

Download the source code used in this article
https://github.com/steverhoades/ZFModuleLoadingExample

The typical order of operations for Zend Applications are:

1. Application/Module bootstrapping
2. Routing
3. Dispatching

Those of you who are reading this article have probably already come to the realization that Zend_Application wants to load ALL resources for ALL modules on ALL requests, this includes execution of ALL plugins defined for your modules. During the bootstrap process Zend attempts to find all the modules that are installed and calls their ModuleName_Bootstrap class, this class defines _initSomething methods to do initializations, regardless of which module is actually being requested.

There are articles out there describing different ways to go about Bootstrapping your modules, once such article here, describes utilizing Module Plugins to handle any initializations after the module has actually been called.

Example Bootstrap

class Foomodule_Bootstrap extends Zend_Application_Module_Bootstrap
{
    public function _initPlugins()
    {
	$front = Zend_Controller_Front::getInstance();
	$front->registerPlugin(new Foomodule_Plugin_Layout());
    }
}

Example Controller Plugin:

class Foomodule_Plugin_Layout extends Zend_Controller_Plugin_Abstract
{
    public function dispatchLoopStartup(Zend_Controller_Request_Abstract $request)
    {
        if ('foomodule' != $request->getModuleName()) {
            // If not in this module, return early
            return;
        }
 
        // Change layout
        Zend_Layout::getMvcInstance()->setLayout('foomodule');
    }
}

The problem with the solution being offered here is that it still requires the application to loop through each of the module bootstrap and plugin classes for each request. With large scale projects, let’s say for the sake of this article 22 modules, performance begins to take a major hit as resources for modules are not being explicitly loaded.

To solve this problem we first must overcome the chicken and the egg problem, how to route to a module that hasn’t been bootstrapped. I have moved all the module configurations to the module.ini file located in the modules configs directory. The module.ini will contain any module specific configurations, most importantly route information.

/modules/users/configs/module.ini in the example code has a custom route:

[production]
routes.users.route = "/:module/blog/comments/:id"
routes.users.defaults.controller = comments
routes.users.defaults.action = review
routes.users.defaults.id = 1
routes.users.reqs.id = "\d+"

Upon request we will loop through all module directories and parse the module configurations (this action can be cached), however we will only bootstrap the Default module and defined module dependencies on every request.

The new order of operations for our Zend_Application:

1. Bootstrap the Application
2. Load Module Configurations
3. Bootstrap Default Module – register ModuleLoader plugin
4. Route
5. During RouteShutdown if requested module does not match Default than attempt to bootstrap requested module as well as module dependencies (Default_Plugin_ModuleLoader)
6. Bootstrap Module and add resources such as controller plugins
7. Dispatch

Application_Resource_Modules (application/resources/Modules.php)

The init() method will grab all the necessary resources from the bootstrap as well as set the class in the Application_Service_Module, this needs to be done due to the fact that the Zend_Application_Resource_ResourceAbstract class will create a new instance everytime Zend_Application_Bootstrap_BootstrapAbstract::getPluginResource($type) is called instead of returning the first instantiated class for that $type.

public function init()
{
    $bootstrap = $this->getBootstrap();
    $bootstrap->bootstrap('frontcontroller');
    $this->_front = $bootstrap->getResource('frontcontroller');
 
    $bootstrap->bootstrap('cachemanager');
    $this->_cache = $bootstrap->getResource('cachemanager')->getCache('default');
 
    /*
     * Get the list of modules
     */
    $this->_modules = $this->_front->getControllerDirectory();
 
    /*
     * Load the module configurations
     */
    $this->_loadModuleConfigs();
 
    /*
     * Bootstrap the Default module on every request
     */
    $this->bootstrapModule('default');
 
    /*
     * Register instance of this class with the application module service.  We
     * need to retain this class due to the fact that the Zend_Application_Resource_ResourceAbstract class
     * will create a new instance everytime a Zend_Application_Bootstrap_BootstrapAbstract::getPluginResource($type)
     * instead of returning the first instantiated class.
     */
     Application_Service_Module::setModuleLoader($this);
 
    return $this->_bootstraps;
}

The bootstrapModule method is essentially a clone of Zend_Application_Resource_Modules::init however has been modified to allow for loading a single known module and it’s module dependencies.

public function bootstrapModule($module)
{
    if(false === array_key_exists($module,$this->_bootstraps)) {
        $moduleDirectory = $this->_modules[strtolower($module)];
        $bootstrapClass = $this->_formatModuleName($module) . '_Bootstrap';
        if (!class_exists($bootstrapClass, false)) {
            $bootstrapPath  = dirname($moduleDirectory) . '/Bootstrap.php';
            if (file_exists($bootstrapPath)) {
                $eMsgTpl = 'Bootstrap file found for module "%s" but bootstrap class "%s" not found';
                include_once $bootstrapPath;
                if (('Default' != $module)
                    && !class_exists($bootstrapClass, false)
                ) {
                    throw new Zend_Application_Resource_Exception(sprintf(
                        $eMsgTpl, $module, $bootstrapClass
                    ));
                } elseif ('Default' == $module) {
                    if (!class_exists($bootstrapClass, false)) {
                        $bootstrapClass = 'Bootstrap';
                        if (!class_exists($bootstrapClass, false)) {
                            throw new Zend_Application_Resource_Exception(sprintf(
                                $eMsgTpl, $module, $bootstrapClass
                            ));
                        }
                    }
                }
            }
        }
 
        $moduleBootstrap = new $bootstrapClass($this->getBootstrap());
        $moduleBootstrap->bootstrap();	        	        
        $this->_bootstraps[$module] = $moduleBootstrap;
 
        /*
         * Check for module dependencies defined by the module.ini
         */
        $moduleBootstraps = $this->getBootstrap()->getOptions();
        if(isset($moduleBootstraps[$module]['module']['dependency'])) {
        	$short = $moduleBootstraps[$module]['module']['dependency'];
        	if(is_array($short)) {
 
        		foreach($short as $moduleDependency) {
 
        			if(!in_array($moduleDependency, array_keys($this->_modules))) {
        				continue;
        			}
 
					$this->bootstrapModule($moduleDependency);
        		}
        	} else {
        		if(in_array($short, array_keys($this->_modules))) {
        			$this->bootstrapModule($short);
        		}
        	}
        }
    }        
}

The _loadModuleConfigs method will loop through all module directories looking for a module.ini file, this method could be updated of course to look for any configuration filetype such as .xml. After it is finished parsing and caching configurations it will grab all routes from the configuration and set them on the defined front controller router object.

final private function _loadModuleConfigs()
{
    $ds = DIRECTORY_SEPARATOR;
    $modules = array_keys($this->_modules);
 
    if(false === ($appOptions = $this->_cache->load('module_configurations'))) {
        $appOptions = $this->getBootstrap()->getOptions();
        foreach ($modules as $module) {
            $filename  = $this->_front->getModuleDirectory($module) . $ds . 'configs'. $ds . 'module.ini';
            if(file_exists($filename)) {
                $cfg = new Zend_Config_Ini($filename, $this->getBootstrap()
                        ->getEnvironment());
                $options = $cfg->toArray();
                if (empty($options)) {
                    continue;
                 }
 
                if (array_key_exists($module, $appOptions) && is_array($appOptions[$module])) {
                    foreach ($options as $key => $value) {
                        if (array_key_exists($key, $appOptions[$module]) && is_array($appOptions[$module][$key])) {
                            $appOptions[$module][$key] = array_merge($appOptions[$module][$key], $value);
                        } else {
                            $appOptions[$module][$key] = $value;
                        }
                    }
                } else {
                    $appOptions[$module] = $options;
                }
            }
        }
        $this->_cache->save($appOptions, 'module_configurations', array(), 9200);            
    }
 
    /*
     * This block checks the appOptions for routes defined in the module.ini and will
     * create a Zend_Config object for each to configure routing properly for Rest and
     * other custom routes per module.
     */
    $router = $this->_front->getRouter();        
    foreach($appOptions as $module => $options) {
        if(in_array($module, $modules) && isset($options['routes'])) {
            $routeName = sprintf("routes_%s", $module);
            $config = new Zend_Config(array($routeName => $options['routes']));
            $router->addConfig($config, $routeName);
        }            
    }
 
    $this->getBootstrap()->setOptions($appOptions);
}

Application_Service_Module (application/services/Module.php)

This class generally would have more utility in a more well defined application, however for this example it simply holds the Bootstrap instance as well as returns the requested module name. The ModuleLoader plugin will call this class to get the module bootstrap object.

class Application_Service_Module
{
    protected static $_moduleLoader;
 
    public static function setModuleLoader($loader)
    {
        self::$_moduleLoader = $loader;
    }
 
    public static function getModuleLoader()
    {
        return self::$_moduleLoader;
    }
 
    public static function getModuleNameFromRequest()
    {
        $front = Zend_Controller_Front::getInstance();
        $module = $front->getRequest()->getModuleName();
 
        return $module;
    }
}

Default_Bootstrap (modules/default/Bootstrap.php)

Register the ModuleLoader controller plugin with the Zend_Controller_Front instance in the Bootstrap.

class Default_Bootstrap extends Zend_Application_Module_Bootstrap
{
	public function _initPluginBrokers()
	{
		$front = Zend_Controller_Front::getInstance();
		$front->registerPlugin(new Default_Plugin_PluginOne());
		$front->registerPlugin(new Default_Plugin_ModuleLoader());
	}
}

Default_Plugin_ModuleLoader (modules/default/plugins/ModuleLoader.php)

This plugin is what will bootstrap the modules, it will pull the module bootstrap instance from the Application_Service_Module class and pass the requested module name to the Application_Resource_Modules method. Notice that if the module matches default we do not want to continue with the module bootstrap process as the default module has already been bootstrapped.

class Default_Plugin_ModuleLoader extends Zend_Controller_Plugin_Abstract
{
    public function routeShutdown(Zend_Controller_Request_Abstract $request)
    {
        if($request->getModuleName() == "default") {
            return;
        }
 
        $moduleBootstrap = Application_Service_Module::getModuleLoader();
        $moduleBootstrap->bootstrapModule($request->getModuleName());
    }
}

Dealing with module dependencies

Occasionally you will have modules that have dependencies on other modules. We will want to insure that these modules are also bootstrapped whenever the dependent module is bootstrapped. You can define module dependencies in the modules configuration file.

[production]
;define a single dependency
module.dependency = auth
 
;OR define multiple dependencies
module.dependency[] = auth
module.dependency[] = users

Further, you can handle module dependencies within your modules Bootstrap class. The following example is not covered in the example code.

class Auth_Bootstrap extends Zend_Application_Module_Bootstrap
{
	public function _initModuleDependencies()
	{
		$moduleBootstrap = Application_Service_Module::getModuleLoader();
		$moduleBootstrap->bootstrapModule('users');
	}	
}

Running through the example application

The example application contains 4 installed modules, a default module, an admin module, an auth module and an users module. All modules will have controller plugins that execute.

If you downloaded the example application available here you should expect to see the following results.

http://yourdomain/

Execution of the default module.

Default_Bootstrap Bootstrap got called
Plugin: Default_Plugin_PluginOne got called
Hello World

http://yourdomain/admin

The admin module has a dependency on the auth module.

Default_Bootstrap Bootstrap got called
Admin_Bootstrap Bootstrap got called
Auth_Bootstrap Bootstrap got called 
Plugin: Admin_Plugin_PluginOne got called
Plugin: Auth_Plugin_PluginOne got called
Admin Index Page

http://yourdomain/users/blog/comments/

The users module defines a custom route.

Default_Bootstrap Bootstrap got called
Users_Bootstrap Bootstrap got called 
Plugin: Users_Plugin_PluginOne got called
Reviewing comments for id 1

http://yourdomain/auth

Default_Bootstrap Bootstrap got called
Auth_Bootstrap Bootstrap got called 
Plugin: Auth_Plugin_PluginOne got called
Auth Index Page

Drawbacks

Although this article has demonstrated how on demand module loading could work with Zend Framework there are a few pitfalls to be aware of.

  1. Because we are utilizing the routeShutdown to bootstrap the modules, module controller plugins will not be able to access the routeStartup or routeShutdown hooks as these events have already been fired off.
  2. Controller plugins must be defined in the modules Bootstrap class, they cannot be defined in the module.ini
  3. Routes cannot be defined in the Bootstrap class, they must be defined in the module.ini file. In this example you should be aware that I left the “resources.router.” out of the route definitions. You can alter this, however you will need to update the Application_Resource_Modules class.
  4. Other resources will need to be also loaded on demand for modules, such as Zend_Navigation configuration (navigation.xml) files.

Resources

http://framework.zend.com/manual/en/zend.application.quick-start.html;
http://weierophinney.net/matthew/archives/234-Module-Bootstraps-in-Zend-Framework-Dos-and-Donts.html;

Feedback

Like this article? Don’t like this article? I want to know! Please provide any feedback you might have.

Follow me on twitter @steverhoades
Connect on linkedin http://linkedin.com/in/steverhoades

Posted in Zend Framework 1.

4 Responses

Stay in touch with the conversation, subscribe to the RSS feed for comments on this post.

  1. Well, the biggest question now is of course what the performance gain is. Do you have any benchmark to show how much faster this method is?

  2. Steve Rhoades said

    Jurian, of course it’s difficult with this small example to show any real performance gains. However, when I get an opportunity I will put together some benchmarks of our project before and after we started lazy loading the modules within our application. The performance gains were dramatic – roughly 30% faster if I remember correctly.

  3. manish singh said

    you can set own layout selector in few steps

    step 1:
    make module admin and default.

    step 2:
    create layout folder in each module as admin/layouts/scripts
    and default/layouts/scripts
    put into layout.phtml

    step 3:
    delete the layout.phtml file from Application/layouts/scripts.

    step 4:
    make the the Plugin folder inside library and make Plugin.php
    as

    class Plugin_Layout extends Zend_Controller_Plugin_Abstract {

    public function preDispatch(Zend_Controller_Request_Abstract $request)
    {
    $layoutPath = APPLICATION_PATH . ‘/modules/’ . $request->getModuleName() . ‘/layouts/scripts/’;
    Zend_Layout::getMvcInstance()->setLayoutPath($layoutPath);
    }
    }

    step 5:

    open Application/configs/Appication.ini file
    and edit it
    as

    ;resources.layout.layoutPath = APPLICATION_PATH “/layouts/scripts/”
    resources.layout.layout = “layout”
    ;register your plugin

    autoloaderNamespaces[] = “Plugin”
    resources.frontController.plugins[] = “Plugin_Layout”

    Step 6:

    open bootstrap file Application/Bootstrap
    put the code inside

    protected function _initAutoload() {

    $loader = new Zend_Application_Module_Autoloader(array(
    ‘namespace’ => ”,
    ‘basePath’ => APPLICATION_PATH . ‘/modules/’
    ));

    return $loader;
    }
    protected function _initPlugins()
    {
    // Access plugin

    $this->bootstrap(‘frontcontroller’);
    $fc = $this->getResource(‘frontcontroller’);
    $fc->registerPlugin(new Plugin_Layout());
    }

Continuing the Discussion

  1. Zend Framework Module | Techtonet linked to this post on August 4, 2012

    [...] http://www.stephenrhoades.com/?p=386 VN:F [1.9.8_1114]Rating: 0 (from 0 votes) [...]

Some HTML is OK

(required)

(required, but never shared)

or, reply to this post via trackback.