• Share
  • Sharebar
  • Share

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