Location: PHPKode > projects > Recess PHP Framework > recess/recess/lang/Annotation.class.php
<?php
Library::import('recess.lang.exceptions.InvalidAnnotationValueException');
Library::import('recess.lang.exceptions.UnknownAnnotationException');

/**
 * Base class for class, method, and property annotations.
 * 
 * @author Kris Jordan <hide@address.com>
 * @copyright 2008, 2009 Kris Jordan
 * @package Recess PHP Framework
 * @license MIT
 * @link http://www.recessframework.org/
 */
abstract class Annotation {
	
	protected $errors = array();
	protected $values = array();
	
	const FOR_CLASS = 1;
	const FOR_METHOD = 2;
	const FOR_PROPERTY = 4;
	
	/* Begin abstract methods */
	
	/**
	 * Returns a string representation of the intended usage of an annotation.
	 * 
	 * @return string
	 */
	abstract public function usage();
	
	/**
	 * Returns an integer representation of the type(s) of PHP language constructs
	 * the annotation is applicable to. Use the Annotation::FOR_* consts to return
	 * the desired result.
	 * 
	 * Examples:
	 *  // Only valid on classes
	 *  function isFor() { return Annotation::FOR_CLASS; }
	 *  
	 *  // Valid on methods or properties
	 *  function isFor() { return Annotation::FOR_METHOD | Annotation::FOR_PROPERTY; }
	 * 
	 * @return integer
	 */
	abstract public function isFor();
	
	/**
	 * Validate is called just before expansion. Because there may be multiple 
	 * constraints of an annotation the implementation of validate should append
	 * any error messages to the protected $errors property. Commonly used validations
	 * helper methods are provided as protected methods on the Annotation class.
	 * 
	 * @param $class The classname the annotation is on.
	 */
	abstract protected function validate($class);
	
	/**
	 * The expansion step of an annotation gives it an opportunity to manipulate
	 * a class' descriptor by introducing additional metadata, attach methods, and
	 * wrap methods.
	 * 
	 * @param string $class Classname the annotation is applied to.
	 * @param mixed $reflection The Reflection(Class|Method|Property) object the annotation is applied to.
	 * @param ClassDescriptor $descriptor The ClassDescriptor being manipulated.
	 */
	abstract protected function expand($class, $reflection, $descriptor);
	
	/* End abstract methods */
	
	/* Begin validation helper methods */
	
	protected function acceptedKeys($keys) {
		foreach($this->parameters as $key => $value) {
			if (is_string($key) && !in_array($key, $keys)) {
				$this->errors[] = "Invalid parameter: \"$key\".";
			}
		}
	}
	
	protected function requiredKeys($keys) {
		foreach($keys as $key) {
			if(!array_key_exists($key, $this->parameters)) {
				$this->errors[] = get_class($this) . " requires a '$key' parameter.";
			}
		}
	}
	
	protected function acceptedKeylessValues($values) {
		foreach($this->parameters as $key => $value) {
			if(!is_string($key) && !in_array($value, $values)) {
				$this->errors[] = "Unknown parameter: \"$value\".";
			}
		}
	}
	
	protected function acceptedIndexedValues($index, $values) {
		if(!in_array($this->parameters[$index],$values)) {
			$this->errors[] = "Parameter $index is set to \"" . $this->parameters[$key] . "\". Valid values: " . implode(', ', $values) . '.';
		}
	}
	
	protected function acceptedValuesForKey($key, $values, $case = null) {
		if(!isset($this->parameters[$key])) { return; }
		
		if($case === null) {
			$value = $this->parameters[$key];
		} else if($case === CASE_LOWER) {
			$value = strtolower($this->parameters[$key]);
		} else if($case === CASE_UPPER) {
			$value = strtoupper($this->parameters[$key]);
		}
		if(!in_array($value, $values)) {
			$this->errors[] = 'The "' . $key . '" parameter is set to "' . $this->parameters[$key] . '". Valid values: ' . implode(', ', $values) . '.';
		}
	}
	
	protected function acceptsNoKeylessValues() {
		$this->acceptedKeylessValues(array());
	}
	
	protected function acceptsNoKeyedValues() {
		$this->acceptedKeys(array());
	}
	
	protected function validOnSubclassesOf($annotatedClass, $baseClass) {
		if( !is_subclass_of($annotatedClass, $baseClass) ) {
			$this->errors[] = get_class($this) . " is only valid on objects of type $baseClass.";
		}
	}
	
	protected function minimumParameterCount($count) {
		if( ! (count($this->parameters) >= $count) ) {
			$this->errors[] = get_class($this) . " takes at least $count parameters.";
		}
	}
	
	protected function maximumParameterCount($count) {
		if( ! (count($this->parameters) <= $count) ) {
			$this->errors[] = get_class($this) . " takes at most $count parameters.";
		}
	}
	
	protected function exactParameterCount($count) {
		if ( count($this->parameters) != $count ) {
			$this->errors[] = get_class($this) . " requires exactly $count parameters.";
		}
	}
	
	/* End validation helper methods */
	
	
	function init($parameters) {
		$this->parameters = array_change_key_case($parameters, CASE_LOWER);
	}
	
	function isAValue($value) {
		return in_array($value, $this->values);
	}
	
	/**
	 * Mask other values to return the first not contained in the array.
	 * 
	 * @param $values
	 * @return value not in the array of other values
	 */
	function valueNotIn($values) {
		foreach($this->values as $value) {
			if(!in_array($value, $values)) {
				return $value;
			}
		}
		return null;
	}
	
	
	function expandAnnotation($class, $reflection, $descriptor) {		
		// First check to ensure this annotation is allowed
		// to apply to this type of PHP construct (class, method, property)
		// using a simple bitwise mask.
		if($reflection instanceof ReflectionClass) {
			$annotationIsOn = self::FOR_CLASS;
			$annotationIsOnType = 'class';
		} else if ($reflection instanceof ReflectionMethod) {
			$annotationIsOn = self::FOR_METHOD;
			$annotationIsOnType = 'method';
		} else if ($reflection instanceof ReflectionProperty) {
			$annotationIsOn = self::FOR_PROPERTY;
			$annotationIsOnType = 'property';
		}
		if(!($annotationIsOn & $this->isFor())) {
			$isFor = array();
			foreach(array('Classes' => self::FOR_CLASS, 'Methods' => self::FOR_METHOD, 'Properties' => self::FOR_PROPERTY) as $key => $mask) {
				if($mask & $this->isFor()) {
					$isFor[] = $key; 
				}
			}
			$this->errors[] = get_class($this) . ' is only valid on ' . implode(', ', $isFor) . '.';
			$typeError = true;
		} else {
			$typeError = false;
		}
		
		// Run annotation specified validations
		$this->validate($class);
		
		// Throw Exception if Annotation Errors Exist
		if(!empty($this->errors)) {
			if($reflection instanceof ReflectionProperty) {
				$message = 'Invalid ' . get_class($this) . ' on property "' . $reflection->getName() . '". ';
				$reflection = new ReflectionClass($class);
			} else {
				$message = 'Invalid ' . get_class($this) . ' on ' . $annotationIsOnType . ' "' . $reflection->getName() . '". ';
			}
			if(!$typeError) {
				$message .= "Expected usage: \n" . $this->usage();
			}
			$message .= "\n == Errors == \n * ";
			$message .= implode("\n * ", $this->errors);
			throw new RecessErrorException($message,0,0,$reflection->getFileName(),$reflection->getStartLine(),array());
		}
		
		// Map keyed parameters to properties on this annotation
		// Place unkeyed parameters on the $this->values array
		foreach($this->parameters as $key => $value) {
			if(is_string($key)) {
				$this->{$key} = $value;
			} else {
				$this->values[] = $value;
			}
		}
		
		// At this point we've processed the parameters, clearing memory
		unset($this->parameters);
		
		// Finally dispatch to abstract method expand() so that
		// Annotation developers can implement glorious new
		// functionalities.
		$this->expand($class, $reflection, $descriptor);
	}
	
	
	/**
	 * Given a docstring, returns an array of Recess Annotations.
	 * @param $docstring
	 * @return unknown_type
	 */
	static function parse($docstring) {
		preg_match_all('%(?:\s|\*)*!(\S+)[^\n\r\S]*(?:(.*?)(?:\*/)|(.*))%', $docstring, $result, PREG_PATTERN_ORDER);
		
		$annotations = $result[1];
		if(isset($result[2][0]) && $result[2][0] != '') {
			$values = $result[2];
		} else { 
			$values = $result[3];
		}
		$returns = array();
		if(empty($result[1])) return array();
		foreach($annotations as $key => $annotation) {
			// Strip Whitespace
			$value = preg_replace('/\s*(\(|:|,|\))[^\n\r\S]*/', '${1}', '(' . $values[$key] . ')');
			// Extract Strings
			preg_match_all('/\'(.*?)(?<!\\\\)\'|"(.*?)(?<!\\\\)"/', $value, $result, PREG_PATTERN_ORDER);
			$quoted_strings = $result[2];
			$value = preg_replace('/\'.*?(?<!\\\\)\'|".*?(?<!\\\\)"/', '%s', $value);
			// Insert Single Quotes
			$value = preg_replace('/((?!\(|,|:))(?!\))(.*?)((?=\)|,|:))/', '${1}\'${2}\'${3}', $value);
			// Array Keyword
			$value = str_replace('(','array(',$value);
			// Arrows
			$value = str_replace(':', '=>', $value);
			
			$value = vsprintf($value . ';', $quoted_strings);
			
			@eval('$array = ' . $value);
			if(!isset($array)) { 
				throw new InvalidAnnotationValueException('There is an unparseable annotation value: "!' . $annotation . ': ' . $values[$key] . '"',0,0,'',0,array());
			}
			
			$annotationClass = $annotation . 'Annotation';
			$fullyQualified = Library::getFullyQualifiedClassName($annotationClass);
			
			if($annotationClass != $fullyQualified || class_exists($annotationClass,false)) {
				$annotation = new $annotationClass;
				$annotation->init($array);
			} else {
				throw new UnknownAnnotationException('Unknown annotation: "' . $annotation . '"',0,0,'',0,get_defined_vars());
			}
			
			$returns[] = $annotation;
		}
		unset($annotations,$values,$result);
		return $returns;
	}
}
?>
Return current item: Recess PHP Framework