Zend Framework supports modules, but in my opinion, not in a modular way. I have been trying to set up self-containing modules with their own configuration. The objective is to have an architecture where I can just drop a module in the modules directory, and it will work immediately without further configuration.

Now that there is Zend Application with the Modules Resource, that is almost possible. There is one problem. The module specific config still has to be put in application.ini. On the Zend Framework mailinglists, many people are clamoring for module specific configuration files. In this article I will explain how to set up modules with their own config files.

Of course as a good developer should, I first started searching for a solution by someone else. I found lots of complaints, but no solutions, until I happened upon Jeroen Keppens’ blog. He has been busy creating a solution to this problem as can be read in this post.

In a nutshell, his solution is this: he has extended Zend_Application_Module_Bootstrap to automatically register and execute a bootstrap resource called Moduleconfig, that searches for ini files in the configs directory of the module. These ini settings are then used as the options for the module bootstrap. This way, a module can set up its own resources.

This is a valid approach and it works, but I had a few problems with it.

  • I found it impossible to load a frontcontroller plugin in the module config. The frontcontroller resource would get called by the module bootstrap, but the frontcontroller bootstrap resource looks at the application options for plugins and ignores module options so it would do nothing.
  • A nitpick: the settings from the module ini files don’t become part of the main application options. This makes them harder to get at in your controllers. Instead of $this->getInvokeArg('bootstrap')->getOptions(), you need to do this:
$this->getInvokeArg('bootstrap')->getResource('modules')->offsetGet('modulenamehere')->getOptions();

I have come up with a solution that is strongly based on Jeroen’s solution, but with an important difference. Instead of letting each module bootstrap get its own config when it is bootstrapped, I chose to create a resource for the main bootstrap that looks for config files in all module config directories and merges them with the application options. This way, the application options array looks the same as if you would have put the ini settings directly in the application.ini.

First, I will show you how to use this solution. Then I’ll share my code with you so you can use it for yourself.

Usage

In your application.ini, you set the resources to be loaded. Note that the resources are loaded in the order you provide them. The Bootstrap resource that is going to do the module setup for us is called Modulesetup. It needs to be loaded as the very first resource, so put it at the top of you [production] segment:

resources.modulesetup[] =

Directly after that I set the modules resource, so that all module-specific resources are loaded first:

resources.modules[] =

Then we create a module.ini in /path/to/application/modules/modulename/configs/module.ini. In that file you can set anything you want, just as if you set it in application.ini. There is one difference: in application.ini you would set it like so:

modulename.resources.frontcontroller.plugins.foo = bar

In your module.ini, you don’t have to set the module prefix, this is done for you:

resources.frontcontroller.plugins.foo = bar

You can now get at your module specific settings in the following ways:

$this->getInvokeArg('bootstrap')->getOption('modulenamehere');

the same but longer:

$this->getInvokeArg('bootstrap')->getResource('modules')->offsetGet('modulenamehere')->getOptions();

And to get your module specific loaded resources:

$this->getInvokeArg('bootstrap')->getResource('modules')->offsetGet('modulenamehere')->getResource('resourcenamehere');

Code

All you need is the following class:

class Rexus_Application_Resource_Modulesetup extends Zend_Application_Resource_ResourceAbstract
{

    public function init()
    {
        $this->_getModuleSetup();
    }

    /**
     * Load the module's ini files
     *
     * @return void
     */
    protected function _getModuleSetup()
    {
        $bootstrap = $this->getBootstrap();

        if (!($bootstrap instanceof Zend_Application_Bootstrap_Bootstrap)) {
            throw new Zend_Application_Exception('Invalid bootstrap class');
        }

        $bootstrap->bootstrap('frontcontroller');
        $front = $bootstrap->getResource('frontcontroller');
        $modules = $front->getControllerDirectory();

        foreach (array_keys($modules) as $module) {
            $configPath  = $front->getModuleDirectory($module)
                         . DIRECTORY_SEPARATOR . 'configs';

            if (file_exists($configPath)) {
                $cfgdir = new DirectoryIterator($configPath);
                $appOptions = $this->getBootstrap()->getOptions();

                foreach ($cfgdir as $file) {

                    if ($file->isFile()) {
                        $filename = $file->getFilename();
                        $options = $this->_loadOptions($configPath
                                 . DIRECTORY_SEPARATOR . $filename);

                        if (($len = strpos($filename, '.')) !== false) {
                            $cfgtype = substr($filename, 0, $len);
                        } else {
                            $cfgtype = $filename;
                        }

                        if (strtolower($cfgtype) == 'module') {
                            if (array_key_exists($module, $appOptions)) {
                                if (is_array($appOptions[$module])) {
                                    $appOptions[$module] =
                                        array_merge($appOptions[$module], $options);
                                } else {
                                    $appOptions[$module] = $options;
                                }
                            } else {
                                $appOptions[$module] = $options;
                            }
                        } else {
                            $appOptions[$module]['resources'][$cfgtype] = $options;
                        }
                    }
                }

                $this->getBootstrap()->setOptions($appOptions);
            } else {
                continue;
            }
        }
    }

    /**
     * Load the config file
     *
     * @param string $fullpath
     * @return array
     */
    protected function _loadOptions($fullpath)
    {
        if (file_exists($fullpath)) {
            switch(substr(trim(strtolower($fullpath)), -3))
            {
                case 'ini':
                    $cfg = new Zend_Config_Ini($fullpath, $this->getBootstrap()
                                                    ->getEnvironment());
                    break;
                case 'xml':
                    $cfg = new Zend_Config_Xml($fullpath, $this->getBootstrap()
                                                    ->getEnvironment());
                    break;
                default:
                    throw new Zend_Config_Exception('Invalid format for config file');
                    break;
            }
        } else {
            throw new Zend_Application_Resource_Exception('File does not exist');
        }

        return $cfg->toArray();
    }
}

Put it in your library directory and don’t forget to set the prefix for your own resources in your application.ini like so:

pluginPaths.Rexus_Application_Resource = "Rexus/Application/Resource"

I use Rexus as the prefix for all my own stuff, but you can change this of course.

I am curious what you think.

edit: comment from Michel: changed the way the module directory is determined from hardcoded to dynamic