Amazium bvba, your online partner
Zend Framework: Module specific config
  • Share post with Twitter
  • Share post with StumbleUpon
  • Share post with Delicious
  • Share post with Digg
  • Share post with Technorati
  • Share post with Blinklist
2009-06-23 23:34

Zend Framework: Module specific config

zend_config, zend_application_resource, zend_application, zend framework

In my everlasting quest to get to an application with self-containing modules, I took a big step today. I added the module specific config loading and a quick acl implementation.

The acl is far from ready, but it was just a test case and due to popular demand I'm already sharing this unfinished piece of code. All this is experimental code, so don't use it in production apps...

Step 1: module contained config files

The documentation says that if you want module specific configuration, you need to add lines to your application.ini that look like:

modulename.resources.resourcename.something = something_else
modulename.some_config = some_value

This works in normal applications, but it's no use for self-containign modules. You want to copy your module in the modules folder and it should work as is.

So, I added a directory "configs" under my module's directory and added 2 files: module.ini and acl.ini. The module.ini will be injected into the module's bootsrap options (cfr. modulename.*). The config in the acl.ini and any other file in the configs dir, will be added as resources.filename.*.

Some quick *fake* examples to explain this:

module.ini

[production]
name = My module name
icon = my_module_icon.png
...

acl.ini

[production]
roles.blog_admin.name = lbl_role_admin
roles.blog_admin.icon = admin.png
roles.blog_user.name = lbl_role_user
roles.blog_user.icon = user.png
roles.blog_visitor.name = lbl_role_visitor
roles.blog_visitor.icon = visitor.png

resources.blog = lbl_module_blog
resources.blog_index = lbl_blog_index_title
resources.blog_index_index = lbl_blog_index_index_title
resources.blog_post = lbl_blog_post_title
resources.blog_post_index = lbl_blog_post_index_title
resources.blog_post_add = lbl_blog_post_add_title

allow.blog_admin.all.resource = blog
allow.blog_user.all.resource = blog
allow.blog_visitor.all.resource = blog_index

This will yield following result if you call getOptions() on your module bootstrap:

array(
    "name"=>"My module name",
    "icon"=>"my_module_icon.png",
    "resources"=>array(
        "acl"=>array(
            "roles"=>array(
                "blog_admin"=>array(
                    "name"=>"lbl_role_admin",
                    "icon"=>"my_moduyle_icon.png"
                ),
                // etc..
            ),
            // etc..
        )
    )
)

This will make sure that you can still use application resources.

Step 2 : loading the config

How did I implemented this? Well, first I needed to extend the Zend_Application_Module_Bootstrap's constructor to allow for an init function to add the config loading (not much error handling yet, pre-beta code):

class Amz_Application_Module_Bootstrap
extends Zend_Application_Module_Bootstrap
{

    /**
     * Constructor
     *
     * @param Zend_Application|Zend_Application_Bootstrap_Bootstrapper $application
     * @return void
     */
    public function __construct($application)
    {
        parent::__construct($application);
        $this->init();
    }

    /**
     * Initialize
     *
     * @return void
     */
    public function init()
    {
    $this->registerPluginResource('moduleConfig');
        $this->_executeResource('moduleConfig');
    }

}

Then we add our application resource:

class Amz_Application_Resource_ModuleConfig 
extends Zend_Application_Resource_ResourceAbstract
{
    
    
/**
     * Initialize
     *
     * @return Zend_Config
     */
    
public function init()
    {
        return 
$this->_getModuleConfig();
    }
    
    
/**
     * Load the module's config
     * 
     * @return Zend_Config
     */
    
protected function _getModuleConfig()
    {
        
$bootstrap $this->getBootstrap();
        if (!(
$bootstrap instanceof Zend_Application_Module_Bootstrap)) {
            throw new 
Zend_Application_Exception('Invalid bootstrap class');
        }
        
$path APPLICATION_PATH DIRECTORY_SEPARATOR 'modules' 
              
DIRECTORY_SEPARATOR $bootstrap->getModuleName()
              . 
DIRECTORY_SEPARATOR 'configs' DIRECTORY_SEPARATOR;
              
        
$cfgdir = new DirectoryIterator($path);
        
$modOptions $this->getBootstrap()->getOptions();
        foreach (
$cfgdir as $file) {
            if (
$file->isFile()) {
                
$filename $file->getFilename();
                
$options $this->_loadOptions($path $filename);
                if ((
$len strpos($filename'.')) !== false) {
                    
$cfgtype substr($filename0$len);
                } else {
                    
$cfgtype $filename;
                }
                if (
strtolower($cfgtype) == 'module') {
                    
$modOptions array_merge($modOptions$options);
                } else {
                    
$modOptions['resources'][$cfgtype] = $options;
                }
            }
        }
        
$this->getBootstrap()->setOptions($modOptions);
    }

    
/**
     * 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();
    }
    
}

This will scan the configs dir and load the config files (ini or xml) as described a bit earlier.

Step 3 : testing with an ACL loader

I wanted to test the config with something, so I decided to make a quick ACL loader. For the default module, you would add lines in application.ini looking like resources.acl.*. For the module you have 3 options:

• application.ini: modulename.resources.acl.*
• module's module.ini: resources.acl.*
• module's acl.ini or acl.xml: * (so no resources.acl prefix)

The Zcl application resource would pick up the acl config per module (if available), and add it to the global Zend_Acl object. This is just a prototype, so might not be 100% as you would do it or even as I would do in real life, but it serves it's purpose as experiment:

class Amz_Application_Resource_Acl
extends Zend_Application_Resource_ResourceAbstract
{
    
    /**
     * Initialize
     *
     * @return Zend_Acl
     */
    public function init()
    {
        return $this->_getAcl();
    }
    
    /**
     * Load the module's acl
     *
     * @return Zend_Acl
     */
    protected function _getAcl()
    {
        // Load the acl from the registry if it doesn't exist
        if (Zend_Registry::isRegistered('Zend_Acl')) {
            $acl = Zend_Registry::get('Zend_Acl');
        } else {
            $acl = new Zend_Acl();
        }

        // Process the config
        $resources = $this->getBootstrap()->getOption('resources');
        if (isset($resources['acl'])) {
            $options = $resources['acl'];
            // Roles
            foreach ($options['roles'] as $role => $info) {
                $acl->addRole(new Zend_Acl_Role($role));
            }
            // Resources
            ksort($options['resources']);
            foreach ($options['resources'] as $resource => $info) {
                if (($pos = strrpos($resource, '_')) !== false) {
                    $parent = substr($resource, 0, $pos);
                } else {
                    $parent = null;
                }
                $acl->add(new Zend_Acl_Resource($resource), $parent);
            }
            // Deny rules        
            foreach ($options['deny'] as $role => $deny) {
                foreach ($deny as $rule) {
                    // Get the resource
                    $resource = isset($rule['resource']) ? trim($rule['resource']) : null;
                    if (strtolower($resource) == 'null' || !$resource) {
                        $resource = null;
                    }
                    // Get the privilege
                    $privilege = isset($rule['privilege']) ? trim($rule['privilege']) : null;
                    if (strtolower($privilege) == 'null' || !$privilege) {
                        $privilege = null;
                    }
                    if (!is_null($privilege)) {
                        $privilege = explode(',', $privilege);
                    }
                    $acl->deny($role, $resource, $privilege);
                }
            }
            // Allow rules
            foreach ($options['allow'] as $role => $allow) {
                foreach ($allow as $rule) {
                    // Get the resource
                    $resource = isset($rule['resource']) ? trim($rule['resource']) : null;
                    if (strtolower($resource) == 'null') {
                        $resource = null;
                    }
                    // Get the privilege
                    $privilege = isset($rule['privilege']) ? trim($rule['privilege']) : null;
                    if (strtolower($privilege) == 'null') {
                        $privilege = null;
                    }
                    if (!is_null($privilege)) {
                        $privilege = explode(',', $privilege);
                    }
                    $acl->allow($role, $resource, $privilege);
                }
            }
        }
    
        // Store the acl back in the registry
        Zend_Registry::set('Zend_Acl', $acl);
        
        // return it
        return $acl;
    }
    
}

The Zend_Acl object can then be picked up later when you want to do authorization with your user's role.

Another approach? Further reading.

There are other approaches to tackle this problem and looks like my post stirred some debate.

Matthijs Van Den Bos started from the module config I did and took another approach: instead of having the configs loaded per module, he loads them all at once.

I had a similar approach in an earlier version of my module config, but I decided that the way to do it with the application resource per module was "cleaner". It DID however introduce problems with code you need called by the frontController, as Matthijs said (see comments below).

You can read about Matthijs solution on his blog: Zend Framework: Module Config.

Leonard Dronkers took a step back and stated that resource loading is basically too heavy for this. You can read his approach on Zend Framework Module Config The Easy Way.

Comments and Feedback

kimpecov

2009-06-24 22:48
I really like this concept. Thank you for sharing your approach, code, etc. Good stuff!
Go on on your quest towards the perfect autarchic self-contained module - that's what is really needed in order to make Zend Framework even sexier than it is already ... :D
Thanks! I hope I get some time to continue the coming days...
Hi,

I'm looking foward on your great work. So far your blog helped me a lot on understanding ZF.

And like Vince said. self-contained modules is realy needed.

I don't understand why it's not already supported in ZF. What's the use of creating modules if each time we have to add a lot a config information inside the main config file.

I do believe that modules should have their own bootstrap, config, controlers, models,views and layouts(if needed) with the posibility to attach generic models of the main application IF needed.
Great post Jeroen!

I have been busy with the same things and have a few questions/remarks.

The way you have set it up, you can't set any frontcontroller plugins in the module.ini. This is because the frontcontroller resource calls getOptions on the application bootstrap and not on the module bootstrap when it is called.

I could write my own resource to handle this for me, but it seems cleaner to me if I could use the standard frontcontroller resource.

The only solution I see is this:
to adapt your ModuleConfig application resource to be called as the first resource from the main bootstrap and have it scan all module directories for ini files and merge them with the main config with the module name as prefix.

This way all module-specific config will be available to all resources from the earliest moment possible.

I have posted about it here and linked back to you.

What do you think?
Hi Matthijs,

I've added a link to your post as a possible different approach / further reading.

I had a similar approach initially, but didn't like the looping over the different modules and solved it by the load-by-module approach.

It *DID* introduce the problem(s) you mention though + it needed the bootstrap constructor to be extended, so I think your approach is also a valid one.

Good comments!
Hi Jeroen,
Thanks for posting this article. It's a good approach for independent configurations!

I had to make one change, because my application resource couldn't be found by the pluginLoader at the first time. I changed your init() by adding a line to add a prefix path to the loader. I changed also the argument from registerPluginResource() from a string to the actual object. These changes made it working for me.

But I have one question: I'd love to use my settings inside the controllers of the module. How can I get the parameters?
I agree that looping through all the modules config directory could not be a good idea generally, but I think that the benefits you can get from the Matthijs solution are more valuable than any other performance issues related to looping.

I mean reasonably maybe you will have how many modules in your application? 10? 15? I don't think that loop through 15 directories can be so stressful.

Isn't it?

In any case I would say thanks to both of you for the great job.

Good job.
Hi Jeroen,

Thanks for your helpful post. But i don't understand how the module.ini is loaded in step 2?

thanks for your explanations
Hi Cyril,

looks like I missed a part when I copied all the posts from the old blog. I have added the moduleconfig application resource now.

I hope it becomes a bit more clear now.

My apologies,

Wkr
Jeroen
Thank you for your anwser !!!
I understand that you have chosen the option of Matthijs for loading the config.
Good article :)
mea culpa, after a new reading it's not the solution of Matthijs but yours.
your solution is perfect for me.

greetings
Cyril
I am beginner in zend framework, I just tested small project albums its working but now I created modules like guestbook , within that I got controller, forms,models and views. everything I did but I don't know how can modify the bootstarp or to add guestbook in bootstrap.

thanks for repling me
I know, old article. But i ran into problems with modules and setting up acl.
I use the modules as standalone modules, so each module has got its own configs, acls and stuff.
Loading config and simple acl will do, but when using some acl asserts it'll fail because the (library) paths to the modules are only available when in the module (module/controller/action) self.
I can create all asserts in a 'common' library, but that will kill the module setup.
I want to provide modules specific database information within the module config file. any suggestions?

AndyFletcheri

2011-04-12 13:21
Hi - I am certainly happy to find this. Good job!
When I register the moduleConfig resource, it started to bug the $this-getResource('FrontController') on line 89 of Zend_Application_Bootstrap_Bootstrap.

If I append $this-bootstrap('frontController') to this line, it stop bugging here and start to bug the helper I've developed for my Views.

The error I get is "Call to a member function getDefaultModule() on a non-object"... Any idea?

Evgen Petrov

2011-09-14 14:05
Hi Jeroen,
Thanks for posting this article.
Update method for version Zend 1.11.10
class Amz_Application_Module_Bootstrap
...
public function init()
{
$resource = "moduleConfig";
$this-registerPluginResource($resource);
$this-getPluginResource($resource);
$this-_executeResource($resource);
}
Correct load plugin ModuleConfig