Pseudo Annotations (@ttribute Oriented Programming) with PHP
Attribute-oriented programming (@OP) is a program-level marking technique. Programmers can mark program elements (e.g. classes and methods) to indicate that they maintain application-specific or domain-specific semantics.
With annotations, we could add some "markers" in a class in an unobtrusive way by annotating a property, methods or class inside it's PHPDoc block. Some of the uses I thought of while doing this article includes.
1. an easy way to inject dependencies (IOC) on methods of a class like an @Inject(Google Guice) annotation.
2. a way in marking a class to map with a persistent layer. e.g @table, @column, etc. like in JPA
3. a way in marking a method to log everytime it is called with @log annotation.
The above examples are just 3 of some hundred of uses with annotation based attribute. And the fun part is, the annotations are totally unobtrusive because it is declared inside a PHPDoc block.
An example of an annotated class in PHP is given below:
<?php ** * User entity * * @author Ronald A. de Leon * @table(name = 'users', type ='innodb') */ class User { /** * username of user * * @var string * @id() * @column(name = 'username', unique = true, updatable = false) */ private $username; /** * @column(name = 'first_name') */ private $firstName; /** * @column(updatable = 'true') */ private $lastName; private $notImportant; /** * transient property * * @var string */ private $fullName; /** * return username * * @return string * @log(type = 'trace') */ public function getUsername() { return $this->username; } /** * return fullName * * @return string */ public function getFullName() { return $this->fullName; } } ?>
We have annotated the User class with pseudo annotations like @id, @column, @table. So let's define our goal:
1. We have to properly extract all our custom-based annotations.
2. We have to do something with our extracted data. ( We will discuss this in Part 2 where we will build a simple EntityManager class for our annotated classes )
Our first goal is be able to retrieve all of the values as well as the annotations provided on the class. There are some considerations that we will be dealing. First, we should not parse default annotations provided by PHPDoc ( @var, @param, etc ). Second, a property, method or class can have zero to multiple annotations, and there should be a rule on where an annotation can only be provided in property but not on the class or method level.
So let's start developing our annotation parser and have to address the goals we have defined. The two goals we have given can be combined into one simple goal: "PROVIDE A LOOKUP FOR ALL THE VALID ANNOTATIONS FOR CLASS, METHOD AND PROPERTY."
To address the issue, we will have a static private property on our parser containing our rule. We can store that in an array. Here's the implementations that I came up with:
<?php private static $validAnnotations = array( 'class' => array( '@table' => array('name', 'type') ), 'properties' => array( '@column' => array('name', 'insertable', 'updatable', 'unique'), '@id' => array('generator'), ), 'methods' => array() ); ?>
The key for the implementation of our parser relies on one of the coolest class in PHP, the ReflectionClass. With the ReflectionClass, we could extract all of the metadata for the passed object including the documentation block for the class, methods and properties.
An example of an implementation of the ReflectionClass is as follows:
$reflect = new ReflectionClass(new User());
After the following line, we could now extract all of our needed data, including getting all of the methods and properties of the User object as well as their PHPDoc if there is one. The key of extracting our annotations lies with the 'getDocComment' method. The 'getDocComment' method returns the PHPDoc block of the given data (Reflection family type e.g. ReflectMethod, etc.)
The next thing will just be some simple string matching (RegEx is your friend here ) on the PHPDoc block with our static private rule property as our validator.
Once we have done this, we could extract our gathered data in some kind of 'AnnotationProxy' object where we could referenced the annotated methods, properties, etc.
The code below shows how I implement this behavior. Downloadable files are attached at the end of this article including the UnitTest for our parser (PHPUnit is needed to run the test).
<?php /** * Parser of the Pseudo annotations * * @author Ronald A. de Leon * */ class AnnotationParser { private static $validAnnotations = array( 'class' => array( '@table' => array('name', 'type') ), 'property' => array( '@column' => array('name', 'insertable', 'updatable', 'unique'), '@id' => array('generator'), ), 'method' => array( '@log' => array('type') ) ); /** uninstantiable, static methods only */ private function __construct() { } public static function createAnnotatedProxy($object) { $r = new ReflectionClass($object); $classAnnotations = self::setProperties(array($r), 'class'); $methodAnnotations = self::setProperties($r->getMethods(), 'method'); $propertiesAnnotations = self::setProperties($r->getProperties(), 'property'); return new AnnotatedProxy($r->name, $classAnnotations, $methodAnnotations, $propertiesAnnotations); } private static function setProperties($properties, $target) { $arrayToSave = array(); foreach($properties as $property) { self::setProperty($property, $target, $arrayToSave); } return $arrayToSave; } private static function setProperty($property, $target, &$arrayToSave) { $propComment = $property->getDocComment(); $arrayToSave[$property->name] = array(); foreach(self::$validAnnotations[$target] as $annotation => $validParameters) { $propAnnotation = self::getAnnotationLine($annotation, $propComment); if($propAnnotation == null || !isset($propAnnotation[1])) { // check if string is not empty if(empty($arrayToSave[$property->name])) { unset($arrayToSave[$property->name]); } continue; } $arrayToSave[$property->name][$annotation] = array(); $lineProps = self::getAnnotationLineProperties($propAnnotation, $validParameters); if($lineProps && !empty($lineProps)) { $arrayToSave[$property->name][$annotation] = $lineProps; } } return $arrayToSave; } private static function getAnnotationLine($name, $searchString) { $matches = array(); preg_match("/{$name}(.*)/", $searchString, $matches); return isset($matches[0]) ? $matches[0] : null; } private static function getAnnotationLineProperties($annotationLine, $validParameters) { $params = array(); preg_match("/((.*))/", $annotationLine, $params); $map = array(); $properties = explode(', ', $params[1]); // $params[0] will return the first match string with '()' foreach($properties as $v) { if(empty($v) || strpos($v, '=') < 0) { continue; } $tmp = explode('=', $v); $key = strtolower(trim($tmp[0])); $value = trim(str_replace('\'', '', $tmp[1])); $value = strtolower($value); if(!in_array($key, $validParameters) || empty($validParameters)) { continue; } $map[$key] = $value; } return $map; } } class AnnotatedProxy { private $classAnnotations; private $methodAnnotations; private $propertyAnnotations; private $className; public function __construct($className, $classAnnotations, $methodAnnotations, $propertyAnnotations) { $this->className = $className; $this->classAnnotations = $classAnnotations; $this->methodAnnotations = $methodAnnotations; $this->propertyAnnotations = $propertyAnnotations; } public function getClassName() { return $this->className; } public function getClassAnnotations() { return $this->classAnnotations; } public function getMethodAnnotations() { return $this->methodAnnotations; } public function getPropertyAnnotations() { return $this->propertyAnnotations; } public function getPropertyAnnotationValue($column, $annotation, $property = null) { if(!array_key_exists($column, $this->propertyAnnotations)) { return null; } if(!array_key_exists($annotation, $this->propertyAnnotations[$column])) { return null; } if($property != null) { return (array_key_exists($property, $this->propertyAnnotations[$column][$annotation])) ? $this->propertyAnnotations[$column][$annotation][$property] : null; } return $this->propertyAnnotations[$column][$annotation]; } public function getMethodAnnotationValue($method, $annotation, $property = null) { if(!array_key_exists($method, $this->methodAnnotations)) { return null; } if(!array_key_exists($annotation, $this->methodAnnotations[$method])) { return null; } if($property != null) { return (array_key_exists($property, $this->methodAnnotations[$method][$annotation])) ? $this->methodAnnotations[$method][$annotation][$property] : null; } return $this->methodAnnotations[$method][$annotation]; } public function getClassAnnotationValue($annotation, $property){ return $this->classAnnotations[$this->className][$annotation][$property]; } } ?>
March 14th, 2008 at 2:38 pm
WoW!! This pseudo annotations function is like tracing all actions done to a certain class just annotate it with @log().. This great… But this pseudo annotations can be use in counting the number of visits for each page?
March 15th, 2008 at 12:49 pm
yes it can, but still you have to write your own implementations for the @log annotation if ever you have defined it in your valid annotaion list.
I think I could demonstrate the @log implementation for the part 2 of this article, including the simple EntityManager that will parse @column, @table annotations we have provided in our example User class.