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($filename, 0, $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.






+32 475 62.42.64
kimpecov
2009-06-24 22:48