Database Driven Zend ACL Tutorial Part One
In this tutorial series, I will guide you through the steps required to replicate my implementation of a database driven Zend ACL. The tutorial will cover the following topics;
- Database structure
- Roles,Resources and Users
- Generating resources from your actions
- Creating the ACL
- Role Inheritance
- Allowing and dis-allowing specific resources for a particular user
Zend ACL Database Structure
First of all you will need to create the database tables. This set of tables will exclude the user table, I’ll leave that out as you may already have a user table which you should be able to link into this set of tables.
The tables required are:
1 2 3 4 5 6 7 8 | CREATE TABLE `Roles` ( `id` INT(11) NOT NULL AUTO_INCREMENT, `name` VARCHAR(255) NOT NULL, `default` tinyint(1) DEFAULT NULL, `modified` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, `created` datetime NOT NULL, PRIMARY KEY (`id`) ) ENGINE=MyISAM DEFAULT CHARSET=utf8; |
1 2 3 4 5 6 7 8 9 10 11 | CREATE TABLE `Resources` ( `id` INT(11) NOT NULL AUTO_INCREMENT, `module` VARCHAR(255) NOT NULL, `controller` VARCHAR(255) NOT NULL, `action` VARCHAR(45) NOT NULL, `name` VARCHAR(255) DEFAULT NULL, `routeName` VARCHAR(255) DEFAULT NULL, `modified` datetime NOT NULL, `created` datetime NOT NULL, PRIMARY KEY (`id`) ) ENGINE=MyISAM DEFAULT CHARSET=utf8; |
1 2 3 4 5 6 7 8 9 10 | CREATE TABLE `RoleResources` ( `id` INT(11) NOT NULL AUTO_INCREMENT, `roleId` INT(11) NOT NULL, `resourceId` INT(11) NOT NULL, `modified` datetime NOT NULL, `created` datetime NOT NULL, PRIMARY KEY (`id`), KEY `role_idx` (`roleId`), KEY `resources_idx` (`resourceId`) ) ENGINE=MyISAM DEFAULT CHARSET=utf8; |
These 3 tables plus a user table, are the minimum required to get a basic database driven Zend ACL working. By basic I mean it will have no role inheritance or the ability to allow or dis-allow resources for a specific user, regardless of role. I will cover those tables in Part Two of this tutorial. The only additional requirement is that you insert a new column into your existing user table and call it roleId. This will be our foreign key into the roles table and will stipulate what role the user has.
Let me just give a quick explanation of each table:
Roles:
The id is self explanatory, this will be the unique id by which the role is identified. The name field again is hopefully self explanatory, as it simply holds the text name of the role. I will go into further detail regarding the default field later in this article, it simply flags what will be the default role. Finally the created and modified fields simply hold dates to identify when a record was created and when it was modified. I add these fields to all the tables I create, the dates, especially the modified field can be helpful when debugging.
Resources:
This table contains the similar fields id, created and modifed as in the roles table. It also contains the module, controller and action fields. These relate to the actual modules, controllers and actions. This implementation uses the actions as resources, and the module, controller and action are the resources unique identifier. The name field, is used as a friendly name for the resource but is not required. The same goes for the routeName field, this is not required it relates to the name of the route stored in your routes, which you may or may not have. In the next section I will show you how to populate this table based on the actions in your controllers.
RolesResources:
Finally this table links resources to roles, it contains the familiar fields, id, created and modified, but also has foreign keys to the roleId and the resourceId, in the Roles and Resources tables, respectively.
So now we have the tables lets populate them with data. Create a couple of roles. I’ve created admin and staff, like so.
1 2 | INSERT INTO `Roles` VALUES (NULL, 'admin', NULL, '2011-02-03 00:00:00', '2011-02-03 00:00:00'); INSERT INTO `Roles` VALUES (NULL, 'staff', NULL, '2011-02-03 00:00:00', '2011-02-03 00:00:00'); |
That’s our basic roles sorted, so what about the resources. We don’t really want to populate the resources by hand, especially in a large project, which is what this implementation was originally written for. So, we’ll get PHP and the Zend Framework to populate them for us using Reflection. I won’t go in to detail as far as Reflection is concerned, it’s out of the scope of this tutorial. In simple terms it allows us to inspect classes at runtime. To generate these resources we will write a class to inspect our controller actions which will then write them to the database. Full credit for this class goes to Ricky Stevens.
Within your Zend installation if you don’t already do this then create a folder within the library folder and give it a name. For the sake of this article I will use my initials DJC. Inside the new folder create a folder called ACL, and then within the ACL folder create a file called Resources.php, as shown below.
/library /Zend /DJC /ACL /Resources.php
The Resources.php is shown in the listing below.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 | class DJC_ACL_Resources { private $arrModules = array(); private $arrControllers = array(); private $arrActions = array(); private $arrIgnore = array('.','..','.svn'); public function __get($strVar) { return ( isset($this->$strVar) ) ? $this->$strVar : null; } public function __set($strVar, $strValue) { $this->$strVar = $strValue; } public function writeToDB() { $this->checkForData(); foreach( $this->arrModules as $strModuleName ) { if( array_key_exists( $strModuleName, $this->arrControllers ) ) { foreach( $this->arrControllers[$strModuleName] as $strControllerName ) { if( array_key_exists( $strControllerName, $this->arrActions[$strModuleName] ) ) { foreach( $this->arrActions[$strModuleName][$strControllerName] as $strActionName ) { // replace this line with your own DB code // write to db if it doesn't exist writeToDB_IfNotExists($strModuleName, $strControllerName, $strActionName); } } } } } return $this; } private function checkForData() { if ( count($this->arrModules) < 1 ) { throw new DJC_ACL_Exception('No modules found.'); } if ( count($this->arrControllers) < 1 ) { throw new DJC_ACL_Exception('No Controllers found.'); } if ( count($this->arrActions) < 1 ) { throw new DJC_ACL_Exception('No Actions found.'); } } public function buildAllArrays() { $this->buildModulesArray(); $this->buildControllerArrays(); $this->buildActionArrays(); return $this; } public function buildModulesArray() { $dstApplicationModules = opendir( APPLICATION_PATH . '/modules' ); while ( ($dstFile = readdir($dstApplicationModules) ) !== false ) { if( ! in_array($dstFile, $this->arrIgnore) ) { if( is_dir(APPLICATION_PATH . '/modules/' . $dstFile) ) { $this->arrModules[] = $dstFile; } } closedir($dstApplicationModules); } public function buildControllerArrays() { if( count($this->arrModules) > 0 ) { foreach( $this->arrModules as $strModuleName ) { $datControllerFolder = opendir(APPLICATION_PATH . '/modules/' . $strModuleName . '/controllers' ); while ( ($dstFile = readdir($datControllerFolder) ) !== false ) { if( ! in_array($dstFile, $this->arrIgnore)) { if( preg_match( '/Controller/', $dstFile) ) { $this->arrControllers[$strModuleName][] = strtolower( substr( $dstFile,0,-14 ) ); } } } closedir($datControllerFolder); } } } public function buildActionArrays() { if( count($this->arrControllers) > 0 ) { foreach( $this->arrControllers as $strModule => $arrController ) { foreach( $arrController as $strController ) { $strClassName = ucfirst( $strModule ).'_'.ucfirst( $strController . 'Controller' ); if( ! class_exists( $strClassName ) ) { Zend_Loader::loadFile(APPLICATION_PATH . '/modules/'.$strModule.'/controllers/'.ucfirst( $strController ).'Controller.php'); } $objReflection = new Zend_Reflection_Class( $strClassName ); $arrMethods = $objReflection->getMethods(); foreach( $arrMethods as $objMethods ) { if( preg_match( '/Action/', $objMethods->name ) ) { $this->arrActions[$strModule][$strController][] = substr($this->_camelCaseToHyphens($objMethods->name),0,-6 ); } } } } } } private function _camelCaseToHyphens($string) { if($string == 'currentPermissionsAction') {$found = true;} $length = strlen($string); $convertedString = ''; for($i = 0; $i <$length; $i++) { if(ord($string[$i]) > ord('A') && ord($string[$i]) < ord('Z')) { $convertedString .= '-' .strtolower($string[$i]); } else { $convertedString .= $string[$i]; } } return strtolower($convertedString); } } } |
To use this class in your controller simply execute the following lines of code:
1 2 3 4 5 | public function generateResourcesAction() { $objResources = new DJC_ACL_Resources(); $objResources->buildAllArrays(); $objResources->writeToDB(); } |
Ok, this class will basically read all the controllers in your application path, and then gather all the methods within each controller and then write them to the database if they don’t already exist. I’ve left the database code as pseudo code so that you can implement your own strategy for writing to a DB. For this depends on the project. For large projects I would use an ORM such as Doctrine, for medium size projects I would probably use Zend_Db for smaller projects I would use my own wrapper class to the PDO library.
That’s it for the first part of this tutorial. In part one we have covered the database tables and how to generate the resources required for the ACL using Zend_Reflection. In part two I will actually get down to business in creating the ACL. Stay tuned!!! Part two shall be posted by the end of this week
UPDATE:Part two has eventually been written and published.
Database Driven Zend ACL Tutorial Part Two




February 8th, 2011 at 2:04 am
[...] This post was mentioned on Twitter by junichi_y, David Clarke. David Clarke said: Database Driven Zend ACL Tutorial Part One | davidjclarke.co.uk http://t.co/PKCtzFb via @ThinkDevGrow [...]
March 9th, 2011 at 10:23 am
Great tutorial, hows it going with Part 2?
March 9th, 2011 at 10:29 am
My apologies Richard, got bogged down with work, I’ll try and get part 2 up by this weekend. Part 2 contains the nitty gritty, and so will hopefully be a more informative read.
April 21st, 2011 at 5:17 pm
Very elegant approach, the most flexible I’m aware about. The key here is the use of Reflection. I still see you didn’t follow-up with part two, though the implementation now should be classical, but with this article I can expect something nice to come from you. So, when part two
April 23rd, 2011 at 11:52 pm
Hi Matt, sorry for the delay. I have just published the second part to this tutorial.
Thanks
Dave
June 5th, 2011 at 4:49 pm
This class assumes a specific directory structure. trying to implement this as is with the default zend application structure without modules will not work.
July 3rd, 2011 at 9:40 pm
@Gregg, Granted you would have to change the class that builds the resources, but this would just be as simple as removing the path reference to /modules in all the build array methods. In the buildModulesArray method, simply set this array to array(‘default’);
Zend Framework by default, although not setup to use a modular structure, the request object will still return a module name, which by default is ‘default’.