PHP 5 Reflection API and Custom-Built ACL for CodeIgniter

One of the projects I'm currently handling was built using CodeIgniter and uses Ben Edmunds Ion Auth for user authentication. I gave another programmer instructions on how to implement a controller-action based access control list (similar to one of CakePHP's ACL implementation). Though not exactly done according to my specs and required some fixes/enhancements, it was fit for consumption 🙂 .

While fixing/debugging the implementation, I had to whip up some function to list all available controllers and actions saving them into the appropriate tables required by the ACL system along with default permissions. I think I requested this feature when I gave out the instructions (no body would want to manually browser and enter all those actions).

Though the administration panel for setting controller-action permissions was easy to use, the client wanted the names of the actions more descriptive so the users have a clear idea on that an action does. The names were the actual action (method) name and should be left untouched, though. Some action names are self-explanatory (edit, send_mail, etc) but it does help if the user can get more info about it. Incidentally, a description field is provided but it is left empty for manual input. Now, the challenge was to find a way to eliminate manual entry of descriptions for each action.

What I did was to use PHP 5's Reflection API. Using PHPDoc style comments, I included special tags (@acl_module_description, @acl_action_description, etc) to add descriptions inside source code comments.

    /**
     *
     * say_cheese
     *
     *
     * @acl_action_description Use unconventional means like voodoo and hypnotism to make the user say cheese
     * @access  public
     */
     function say_cheese(){
     
     }


So along with the code that sets up the available controllers/action, I added some code to get the comments for each controller/action using ReflectionClass::getDocComment() and ReflectionMethod::getDocComment(), did some parsing to extract the tags and get the value of description-related tag and use it as the description for the action.

    // taken from http://www.phpriot.com/articles/reflection-api/4
    function __commentToArray($sDocComment = ''){
	$sDocComment = preg_replace("/(^[\\s]*\\/\\*\\*)
				     |(^[\\s]\\*\\/)
				     |(^[\\s]*\\*?\\s)
				     |(^[\\s]*)
				     |(^[\\t]*)/ixm", "", $sDocComment);

	$sDocComment = str_replace("\r", "", $sDocComment);
	$sDocComment = preg_replace("/([\\t])+/", "\t", $sDocComment);
	$aDocCommentLines = explode("\n", $sDocComment);
	
	return $aDocCommentLines;
    }
    
    function __getDocTags($docCommentArray){
	
	$tags = array();
	$currentTag = null;
	foreach ($docCommentArray as $line) {
	    $line = trim($line);
	    
	    
	    if (isset($line[0]) && $line[0] == '@') {
		
		$lineArray = explode(' ', $line);
		$lineArray = array_reverse($lineArray);
		
		$currentTag = str_replace('@', '', array_pop($lineArray));
		
		$lineArray = array_reverse($lineArray);
		$line = implode(' ', $lineArray);
		
		$tags[$currentTag] = $line;
		
	    } else {
		if ($currentTag !== null && $line != '*/') {
		    $tags[$currentTag] .= "\n" . $line;
		}
	    }
	}
	
	return $tags;
    }

The code looks something like this:

function get_this_class_methods($class){
    $array1 = get_class_methods($class);
    if($parent_class = get_parent_class($class)){
        $array2 = get_class_methods($parent_class);
        $array3 = array_diff($array1, $array2);
    }else{
        $array3 = $array1;
    }
    return($array3);
}



	$controller_path = APPPATH . 'controllers';

	$controllers = array();
	if ($handle = opendir($controller_path)) {
	    while (false !== ($file = readdir($handle))) {
		if ($file != "." && $file != "..") {
		    if (strpos($file, '.php')) {
			$class = explode('.php', $file);
			array_pop($class);
			$class = ucwords(implode('', $class));

			if (!class_exists($class)) {
			    require_once($controller_path . '/' . $file);
			}

			$actions = get_this_class_methods($class);

			// Get Reflection Class
			$classReflection = new ReflectionClass($class);
			$classComment = $classReflection->getDocComment();
			$classTags = $this->__getDocTags($this->__commentToArray($classComment));
			
			$classDescription = '';
			
			if (isset($classTags['acl_module_description'])) {
			    $classDescription = $classTags['acl_module_description'];
			}
			
			$public_actions = array();

			foreach ($actions as $a) {
			    if ($a === $class || strpos($a, '_') === 0){
				continue;
			    }
			    
			    
			    // Get Reflection method
			    $methodReflection = $classReflection->getMethod($a);
			    
			    
			    // Get Comment
			    
			    $comment = $methodReflection->getDocComment();
			    
			    $description = '';
			    $is_locked = 0;
			    
			    if ($comment) {
				$tags = $this->__getDocTags($this->__commentToArray($comment));
				
				if (isset($tags['acl_action_description'])) {
				    $description = $tags['acl_action_description'];
				}
				
				if (isset($tags['acl_action_locked'])) {
				    $is_locked = 1;
				}
				
			    }

			    $public_actions[$a] = array(
				'description' => $description,
				'is_locked' => $is_locked,
			    );
			}


			$controllers[$class] = array(
			    'description' => $classDescription,
			    'actions' => $public_actions,
			);



		    }
		}
	    }
	    closedir($handle);
	}

(Yes, I wasn't using Reflection to get the methods. Better remind myself to change that to be consistent)

Another thing I was able to accomplish with this approach is to hide/lock certain actions from being displayed or edited in the admin panel. For example, login related actions and custom validation methods (which can be used as actions) should not be messed around with by the user. In this case, I used another tag (@acl_action_locked) - mere presence of this tag in the function comment means that the action should be hidden/locked.

Nice, huh? The description needs to be done only once (inside the comment) and from there we can automate its entry to the database. I can just leave task for adding descriptions to the programmers working on the controllers/actions - since they are the ones who really now what their code does :).

 

Comments

No comments so far.

Leave a Reply

 
(will not be published)
 
 
Comment
 
 

 

Resources