Jump to content

annotation critique


Destramic
 Share

Recommended Posts

hey guys,

 

i discovered annotation validation recently and liked how easy it is to validate general class' and form enitys...so i decided i'd write my own script...i don't normally ask for feedback, but as i jumped into the deep end creating an annotation handler, with no experience of using/writing one, i'd like to ask if i could get some general feedback please.

 

i've read some documation on symfony2 and phpdoc but that's about it.

 

annotation reader: reads and gathers comments left for properties and mehtods as well as namespaces

<?php

namespace Annotation;

use Exception;
use ReflectionClass;
use ReflectionProperty;
use ReflectionMethod;
use ReflectionFunction;

class Reader
{	
	private $class_name;
	
	private $properties = array();
	private $methods    = array();
	private $namespaces = array();
	
	public function read($object)
	{
		$reflector        = new ReflectionClass($object);
		$this->class_name = $reflector->getShortName();
		
		foreach ($reflector->getProperties() as $property)
		{
			$name     = $property->getName();
			$property = $reflector->getProperty($name);
			
			$property->setAccessible(true);
			
			$value  = $property->getValue($object);
			$tags   = new Phaser($property->getDocComment());
			
			$this->properties[$name] = array(
					'tags'  => $tags->phase(),
					'value' => $value
			);
		}
		
		foreach ($reflector->getMethods() as $method)
		{
			$name   = $method->getName();
			$method = $reflector->getMethod($name);
			
			$method->setAccessible(true);

			$tags        = new Phaser($method->getDocComment());
			$parameters  = array();
			
		 	foreach ($method->getParameters() as $parameter) 
		 	{
        		$parameters[] = strtolower($parameter->name);
    		}
    		
			$this->methods[$name] = array(
					'tags'        => $tags->phase(),
					'parameters'  => $parameters
			);
		}
		
		$contents = file_get_contents($reflector->getFileName());
		
		preg_match_all('#use\s+(?P<namespace>[\w\\\]+)(?:\s+as\s+(?P<alias>[\w]+))?#i', $contents, $matches);
				
		for ($i = 0; $i < count($matches[0]); $i++)
		{
			$alias     = strtolower($matches['alias'][$i]);
			$namespace = strtolower($matches['namespace'][$i]);
			
			if (empty($alias))
			{
				$alias = $namespace;
			}
			
			$this->namespaces[$alias] = $namespace;
		}
		
		return $this;
	}
	
	public function get_class_name()
	{
		return $this->class_name;
	}
	
	public function get_properties()
	{
		return $this->properties;
	}
	
	public function get_property($property)
	{
		if (isset($this->properties[$property]))
		{
			return $this->properties[$property];
		}
		
		return null;
	}
	
	public function get_methods()
	{
		return $this->methods;
	}
	
	public function get_method($method)
	{
		if (isset($this->methods[$method]))
		{
			return $this->methods[$method];
		}
		
		return null;
	}
	
	public function get_namespaces()
	{
		return $this->namespaces;
	}
}

annotation phaser: used in the reader, this phases the tags and returns a nice array?

<?php

namespace Annotation;

class Phaser
{	
	private $annotation;
	
	private $tags = array();
	
	private $patterns = array(
		'vars_params' => '@(?P<tag>var|param)\s+(?:\\$(?P<parameter>\w+)\s+)?(?P<type>\S+) *(?P<description>.*)?',
	    'return'      => '@return\s+(?P<type>\S+) *(?P<description>.*)?'
	);
	
	public function __construct($annotation)
	{
		$this->annotation = $annotation;
	}
	
	public function phase()
	{
		foreach ($this->patterns as $tag => $pattern)
		{
			$this->$tag();
		}
		
		return $this->tags;
	}
	
	private function vars_params()
	{
		$vars_params = $this->match($this->patterns['vars_params']);
		
		$vars   = array();
		$params = array();
		
		if ($vars_params)
		{
			for ($i = 0; $i < count($vars_params[0]); $i++)
			{
				$tag       = $vars_params['tag'][$i] . 's';
				$parameter = strtolower($vars_params['parameter'][$i]);
				
				$data = array(
						'type'       => $vars_params['type'][$i],
					    'descrption' => $vars_params['description'][$i]
				);
				
				if ($tag === 'vars')
				{
					${$tag}[] = $data;
				}
				else 
				{
					${$tag}[$parameter][] = $data;
				}
			}
		}

		if (count($vars) > 0)
		{
			$this->tags['vars'] = $vars;
		}
		else if (count($params) > 0)
		{
			$this->tags['params'] = $params;
		}
	}
	
	private function return()
	{
		$return = $this->match($this->patterns['return']);

		if ($return)
		{
			$this->tags['return'] = array(
					'type'        => $return['type'][0],
			   	 	'description' => $return['description'][0]
			);
		}
	}
	
	private function match($pattern)
	{
		$pattern = '#' . $pattern . '#';
	
		if (preg_match_all($pattern, $this->annotation, $matches))
		{
			return $matches;
		}
	
		return null;
	}
}	

ok so i was a little confused to how the annotation validation process is kicked off, after a lot of thinking all i could come up was this:

 

annotation: where my class is injected so it can be validated on __call  & __set

<?php

namespace Annotation;

use Exception;
use ReflectionClass;

class Annotation
{	
	private $object;
	private $reader;
	private $validator;
	
	public function __construct($object)
	{
		if (!is_object($object))
		{
			throw new Exception('Validator: Unkown class object.');
		}
		
		$this->object    = $object;
		$reader          = new Reader;
		$this->reader    = $reader->read($object);
		$this->validator = new Validator($reader);
	}

	public function __set($property, $value)
	{
		$this->validator->set_property($property)
		                ->set_value($value)
		                ->validate_property()
		                ->reset();
		
		$this->object->$property = $value;
	}
	
	public function __get($property)
	{
		return $this->object->$property;
	}	
	
	public function __call($method, $arguments = array())
	{
		$this->validator->set_method($method)
		                ->set_arguments($arguments)
		                ->validate_parameters();
		
		$return = call_user_func_array(array(
				$this->object, 
				$method), 
				$arguments
		);
		
		$this->validator->set_return($return)
		                ->validate_return()
		                 ->reset();
		
		return $return;
	}
	
	// entitys 
	public function validate()
	{
		
	}
}

and finally my validator, which validates propery/method var, parma, return, constraints

<?php

namespace Annotation;

use Exception;

class Validator
{	
	private $property;
	private $method;
	private $value;
	
	private $arguments = array();
	
	private $reader;

	public function __construct(Reader $reader)
	{
		$this->reader = $reader;
	}
	
	public function validate_parameters()
	{
		$method = $this->get_method();
		
		if (empty($method['tags']['params']))
		{
			return $this;
		}
		
		$i         = 0;
		$arguments = $this->get_arguments();
		
		foreach ($method['parameters'] as $parameter)
		{
			if (!isset($method['tags']['params'][$parameter]))
			{
				continue;
			}
			
			foreach ($method['tags']['params'][$parameter] as $tag)
			{
				$type      = $tag['type'];
				$value     = null;
				
				if (isset($arguments[$i]))
				{
					$value = $arguments[$i];
				}
					
				if ($match = $this->is_namespace($type))
				{
					if (!$this->validate_namespace($match['namespace']))
					{
						throw new Exception(sprintf("Validator: Parameter $%s must instance of %s.", $parameter, $match['namespace']));
					}
				}
				else if ($match = $this->is_constraint($type))
				{
					$response = $this->validate_constraint($match['alias'], $match['method'], $match['arguments'], $value);
					
					if ($response !== true)
					{
						throw new Exception(sprintf("Validator: Parameter $%s %s", $parameter, $response));
					}
				}
				else
				{
					if (!$this->is_valid_data_type($type, $value))
					{
						throw new Exception(sprintf("Validator: Parameter $%s must be %s type.", $parameter, $type));
					}
				}
			}
			
			$i++;
		}
		
		return $this;
	}

	public function validate_return()
	{
		$method = $this->get_method();

		if (!isset($method['tags']['return']))
		{
			return $this;
		}
		
		$type  = $method['tags']['return']['type'];
		$value = $this->get_return();
		
		if (!$this->is_valid_data_type($type, $value))
		{
			throw new Exception(sprintf("Validator: Method %s must return %s type.", $this->method, $type));
		}
	
		return $this;
	}
	
	public function validate_property()
	{
		$property = $this->get_property();

		if (!isset($property['tags']['vars']))
		{
			return $this;
		}
		
		$value = $this->get_value();
		
		foreach ($property['tags']['vars'] as $tag)
		{
			$type = $tag['type'];
			
			if ($match = $this->is_namespace($type))
			{
				if (!$this->validate_namespace($match['namespace']))
				{
					throw new Exception(sprintf("Validator: Property $%s must instance of %s.", $parameter, $match['namespace']));
				}
			}
			else if ($match = $this->is_constraint($type))
			{
				$response = $this->validate_constraint($match['alias'], $match['method'], $match['arguments'], $value);
					
				if ($response !== true)
				{
					throw new Exception(sprintf("Validator: Property $%s %s", $this->property, strtolower($response)));
				}
			}
			else
			{
				if (!$this->is_valid_data_type($type, $value))
				{
					throw new Exception(sprintf("Validator: Property $%s must be %s type.", $this->property, $type));
				}
			}
		}
		
		return $this;
	}
	
	private function is_valid_data_type($type, $value)
	{
		$type = explode('|', $type);
		
		foreach ($type as $type)
		{
			if (strcasecmp($type, gettype($value)) === 0)
			{
				return true;
			}
		}
		
		return false;
	}
	
	private function validate_constraint($alias, $method, $arguments, $value)
	{
		$alias      = strtolower($alias);
		$namespaces = $this->reader->get_namespaces();
		
		if (!array_key_exists($alias, $namespaces))
		{
			throw new Exception(sprintf("Validator: Uknown constraint %s.", $type));
		}
		
		$class = $namespaces[$alias];
		
		if (!class_exists($class))
		{
			throw new Exception(sprintf("Validator: Unknown constraint %s."), $class);
		}
		
		$constraint = new $class;
		$method     = $method;
		
		if (!is_array($arguments))
		{
			$arguments = array($arguments);
		}
		
		$constraint = call_user_func_array(array($constraint, $method), $arguments);
			
		$constraint->set_value($value);
		
		if ($constraint->is_valid())
		{
			return true;
		}
		
		return $constraint->get_error_message();
	}
	
	private function validate_namespace($namespace, $parameter)
	{
		$namespace = strtolower($namespace);
		
		$aliases = $this->reader->get_namespaces();
		
		if (array_key_exists($namespace, $aliases))
		{
			$namespace = $aliases[$namespace];
		}
		
		if ($value instanceof $namespace)
		{
			return true;
		}
		
		return false;
	}
	
	private function is_namespace($string)
	{
		$types = array(
				'booleon',
				'integer',
				'double',
				'string',
				'array',
				'object'.
				'resource',
				'null'
		);
		
		if (!in_array(strtolower($string), $types) && preg_match('#^(?P<namespace>(?:\\\\)?[\w]+(?:\\\\[\w]+)*)$#', $string, $match))
		{
			return $match;
		}
		
		return false;
	}
	
	private function is_constraint($string)
	{
		if (preg_match('#^(?P<alias>\w+)\\\(?P<method>\w+)\((?P<arguments>.*)\)$#', $string, $match))
		{
			return $match;
		}
		
		return false;
	}
	
	public function set_method($method)
	{
		$this->method = $method;
		
		return $this;
	}
	
	public function get_method()
	{
		if (is_null($this->method))
		{
			throw new Exception('Validator: Method has not been set.');
		}
		else if (!$method = $this->reader->get_method($this->method))
		{
			throw new Exception(sprintf('Unkown method %s in a class %s.', $this->method, $this->reader->get_class_name()));
		}
		
		return $method;
	}
	
	public function set_arguments($arguments)
	{
		$this->arguments = $arguments;
		
		return $this;
	}
	
	public function get_arguments()
	{
		if (!is_array($this->arguments))
		{
			throw new Exception('Validator: Arguments must be an array.');
		}
		
		return $this->arguments;
	}
	
	public function get_property()
	{
		if (is_null($this->property))
		{
			throw new Exception('Validator: Property has not been set.');
		}
		else if (!$property = $this->reader->get_property($this->property))
		{
			throw new Exception(sprintf('Unkown property $%s in a class %s.', $this->property, $this->reader->get_class_name()));
		}
		
		return $property;
	}
	
	public function set_property($property)
	{
		$this->property = $property;
		
		return $this;
	}
	
	public function set_value($value)
	{
		$this->value = $value;	
		
		return $this;
	}
	
	public function get_value()
	{
		return $this->value;
	}
	
	public function set_return($return)
	{
		$this->return = $return;
		
		return $this;
	}
	
	public function get_return()
	{
		return $this->return;
	}
	
	public function reset()
	{
		$this->method    = null;
		$this->arguments = array();
		$this->value     = null;
		$this->return    = null;
		
		return $this;
	}
}

how my annotation validation works on a test class (Person)

use Validator\Constraint as Assert;

class Person
{
	/**
	 * @var Assert\Not_Blank()
	 * @var string
	 **/
	public $name;
	
	/**
	 * @var Assert\Not_Blank()
	 * @var integer
	 **/
	public $age;
	
	/**
	 * @var Assert\Email_Address()
	 * @var string
	 **/
	public $email_address;
	
	/**
	 * @return array
	 **/
	public function get_credentials()
	{
		// working 
		return array(
				'name'          => $this->name,
				'age'           => $this->age,
				'email_address' => $this->email_address
		);
	}
}

use Annotation\Annotation as Annotation;

$person = new Annotation(new Person);
$person->name = 'John Doe';
$person->age  = 101;
$person->email_address = 'someone@homtmail.com';

print_r($person->get_credentials());

its all singing and dancing, although there is still a lot to do like implment a gernal validation for single/multiple entitys and tidying up....but i really don't know if i've gone around the right way of doing this...have i over thought it?

 

and i know its a lot to look at but any feedback/criticism would be appreciated

 

 

thakn you!

Edited by Destramic
Link to comment
Share on other sites

I haven't read the entire code, but I'm fairly sure you can make it a lot simpler and more robust if you use the official phpDocumentor parser instead of implementing your own (by the way, it's parse, not “phase”).

 

thanks for tip...on reflection of my attemp above i can see it's very poor...i'll give phpDocumentor a look :)

Link to comment
Share on other sites

This thread is more than a year old.

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.

Guest
Reply to this topic...

×   Pasted as rich text.   Restore formatting

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.

 Share

×
×
  • Create New...

Important Information

We have placed cookies on your device to help make this website better. You can adjust your cookie settings, otherwise we'll assume you're okay to continue.