├── .gitignore ├── lib ├── Exar │ ├── Annotation │ │ ├── Matcher │ │ │ ├── Matcher.php │ │ │ ├── AbstractMatcher.php │ │ │ ├── SingleValueMatcher.php │ │ │ ├── ValueMatcher.php │ │ │ ├── NumberMatcher.php │ │ │ ├── KeyValuePairMatcher.php │ │ │ ├── QuotedValueMatcher.php │ │ │ ├── RegexMatcher.php │ │ │ ├── StringMatcher.php │ │ │ ├── ArrayMatcher.php │ │ │ ├── ParallelMatcher.php │ │ │ ├── StringNotMatchedException.php │ │ │ ├── SequentialMatcher.php │ │ │ ├── ConstantsMatcher.php │ │ │ ├── SeparatedValuesMatcher.php │ │ │ └── AnnotationsMatcher.php │ │ ├── SimpleAnnotation.php │ │ ├── AnnotationParser.php │ │ └── Annotation.php │ ├── Aop │ │ ├── Interceptor │ │ │ ├── Interfaces │ │ │ │ ├── AfterThrowingInterceptor.php │ │ │ │ ├── AfterReturningInterceptor.php │ │ │ │ ├── BeforeInvocationInterceptor.php │ │ │ │ └── AfterInvocationInterceptor.php │ │ │ ├── MediaType.php │ │ │ ├── Path.php │ │ │ ├── PostConstruct.php │ │ │ └── Track.php │ │ ├── AnnotationProcessor.php │ │ ├── InterceptorException.php │ │ ├── InvocationContext.php │ │ ├── InterceptorManager.php │ │ └── Weaver.php │ ├── Reflection │ │ ├── ReflectionMethod.php │ │ ├── ReflectionProperty.php │ │ ├── ReflectionInterface.php │ │ ├── AnnotationContainer.php │ │ └── ReflectionClass.php │ ├── Autoloader.php │ └── RestHandler.php └── bootstrap.php ├── test ├── Exar │ ├── TestClasses │ │ ├── NoExarAnnotatedClass.php │ │ ├── NoExarButInheritsFromExarClass.php │ │ ├── ClassWithCustomAnnotation.php │ │ ├── MyAnnotationAnnotatedClass.php │ │ ├── Annotation │ │ │ ├── CustomAnnotation.php │ │ │ └── MultipleAnnotation.php │ │ ├── ClassAnnotatedClass.php │ │ ├── ClassWithoutConstructor.php │ │ ├── ClassAnnotatedClassForAnnotationContainer.php │ │ ├── ClassWithPostConstruct.php │ │ ├── ClassWithParentConstructor.php │ │ ├── ClassWithParentsParentConstructor.php │ │ ├── ClassWithComments.php │ │ ├── ClassWithConstructorParams.php │ │ └── ExarAnnotatedClass.php │ ├── Aop │ │ └── Interceptor │ │ │ ├── MyAnnotation.php │ │ │ ├── MyAnnotationTest.php │ │ │ └── PostConstructTest.php │ ├── ExarTest.php │ ├── SimpleTest.php │ ├── Annotation │ │ ├── Matcher │ │ │ ├── StringMatcherTest.php │ │ │ ├── NumberMatcherTest.php │ │ │ └── ArrayMatcherTest.php │ │ └── AnnotationParserTest.php │ ├── Reflection │ │ ├── ReflectionClassTest.php │ │ └── AnnotationContainerTest.php │ └── WeaverTest.php └── bootstrap.php ├── composer.json ├── phpunit.xml.dist ├── LICENSE └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | _cache/ 3 | vendor/ 4 | *.iml 5 | composer.lock -------------------------------------------------------------------------------- /lib/Exar/Annotation/Matcher/Matcher.php: -------------------------------------------------------------------------------- 1 | matchers[] = $matcher; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /test/bootstrap.php: -------------------------------------------------------------------------------- 1 | value; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /test/Exar/TestClasses/ClassAnnotatedClass.php: -------------------------------------------------------------------------------- 1 | $value); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /lib/Exar/Annotation/Matcher/ValueMatcher.php: -------------------------------------------------------------------------------- 1 | add(new ArrayMatcher()); 7 | $this->add(new QuotedValueMatcher()); 8 | $this->add(new ConstantsMatcher()); 9 | $this->add(new NumberMatcher()); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /lib/Exar/Annotation/SimpleAnnotation.php: -------------------------------------------------------------------------------- 1 | _name = $annotationName; 8 | } 9 | 10 | public function getValue() { 11 | return $this->value; 12 | } 13 | } -------------------------------------------------------------------------------- /test/Exar/TestClasses/ClassWithoutConstructor.php: -------------------------------------------------------------------------------- 1 | value; 12 | } 13 | 14 | /** 15 | * @PostConstruct 16 | */ 17 | public function init() { 18 | $this->value = 345; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /test/Exar/TestClasses/ClassAnnotatedClassForAnnotationContainer.php: -------------------------------------------------------------------------------- 1 | assertTrue($class->hasAnnotation('MyAnnotation')); 12 | } 13 | 14 | } 15 | -------------------------------------------------------------------------------- /test/Exar/TestClasses/ClassWithPostConstruct.php: -------------------------------------------------------------------------------- 1 | value; 15 | } 16 | 17 | /** 18 | * @PostConstruct 19 | */ 20 | public function init() { 21 | $this->value = 123; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /lib/Exar/Annotation/Matcher/NumberMatcher.php: -------------------------------------------------------------------------------- 1 | anotherValue; 12 | } 13 | 14 | /** 15 | * @PostConstruct 16 | */ 17 | public function anotherInit() { 18 | $this->anotherValue = 789; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /test/Exar/TestClasses/ClassWithParentsParentConstructor.php: -------------------------------------------------------------------------------- 1 | anotherValue; 12 | } 13 | 14 | /** 15 | * @PostConstruct 16 | */ 17 | public function anotherInit() { 18 | $this->anotherValue = 654; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /lib/Exar/Annotation/Matcher/KeyValuePairMatcher.php: -------------------------------------------------------------------------------- 1 | add(new RegexMatcher(AnnotationsMatcher::REGEX_PARAMETER_NAME)); 7 | $this->add(new StringMatcher('=')); 8 | $this->add(new ValueMatcher()); 9 | } 10 | 11 | public function match(&$str) { 12 | $arr = parent::match($str); 13 | return array($arr[0]=>$arr[1]); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /lib/Exar/Annotation/Matcher/QuotedValueMatcher.php: -------------------------------------------------------------------------------- 1 | add(new RegexMatcher(AnnotationsMatcher::REGEX_PARAMETER_SINGLE_QUOTED_VALUE)); 7 | $this->add(new RegexMatcher(AnnotationsMatcher::REGEX_PARAMETER_DOUBLE_QUOTED_VALUE)); 8 | } 9 | 10 | public function match(&$str) { 11 | $value = parent::match($str); 12 | return substr($value, 1, strlen($value) - 2); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /lib/bootstrap.php: -------------------------------------------------------------------------------- 1 | =5.3", 15 | "ext-tokenizer": "*", 16 | "nikic/php-parser": "1.0.*@dev", 17 | "nikic/fast-route": "dev-master" 18 | }, 19 | "autoload": { 20 | "files": ["lib/bootstrap.php"] 21 | }, 22 | "minimum-stability": "dev" 23 | } 24 | -------------------------------------------------------------------------------- /lib/Exar/Annotation/Matcher/RegexMatcher.php: -------------------------------------------------------------------------------- 1 | pattern = $pattern; 9 | } 10 | 11 | public function match(&$str) { 12 | if (preg_match("/^{$this->pattern}/", $str, $matches) && $matches[0] != '') { 13 | $str = substr($str, strlen($matches[0])); 14 | return $matches[0]; 15 | } 16 | 17 | throw new StringNotMatchedException($this, $str, $this->pattern); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /lib/Exar/Aop/Interceptor/MediaType.php: -------------------------------------------------------------------------------- 1 | value); 16 | return $result; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /lib/Exar/Annotation/Matcher/StringMatcher.php: -------------------------------------------------------------------------------- 1 | string = $string; 9 | } 10 | 11 | public function match(&$str) { 12 | $length = strlen($this->string); 13 | 14 | if(strlen($str) >= $length && substr($str, 0, $length) == $this->string) { 15 | $str = substr($str, $length); 16 | return array(); 17 | } 18 | 19 | throw new StringNotMatchedException($this, $str, $this->string); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /lib/Exar/Annotation/Matcher/ArrayMatcher.php: -------------------------------------------------------------------------------- 1 | matchers = array(); 7 | 8 | $this->add(new StringMatcher('{')); 9 | 10 | $pMatcher = new ParallelMatcher(); 11 | $pMatcher->add(new SeparatedValuesMatcher(new ValueMatcher())); // for arrays with at least 1 value 12 | $pMatcher->add(new StringMatcher()); // for empty arrays 13 | $this->add($pMatcher); 14 | 15 | $this->add(new StringMatcher('}')); 16 | 17 | $arr = parent::match($str); 18 | return $arr; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /test/Exar/TestClasses/ClassWithComments.php: -------------------------------------------------------------------------------- 1 | anotherValue; 16 | } 17 | 18 | /** 19 | * @PostConstruct 20 | */ 21 | public function anotherInit() { 22 | $this->anotherValue = 789; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /lib/Exar/Annotation/Matcher/ParallelMatcher.php: -------------------------------------------------------------------------------- 1 | matchers as $matcher) { 8 | $toParse = $str; 9 | try { 10 | $result = $matcher->match($toParse); 11 | $str = $toParse; 12 | return $result; // return first result that was parsed successfully 13 | } catch (StringNotMatchedException $e) { 14 | // do nothing 15 | } 16 | } 17 | throw new StringNotMatchedException($this, $str, ''); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 13 | 14 | 15 | ./test/ 16 | 17 | 18 | 19 | 20 | 21 | ./lib/Exar/ 22 | 23 | 24 | -------------------------------------------------------------------------------- /lib/Exar/Annotation/Matcher/StringNotMatchedException.php: -------------------------------------------------------------------------------- 1 | matcher = $matcher; 11 | $this->str = $str; 12 | $this->strToMatch = $strToMatch; 13 | } 14 | 15 | public function getStr() { 16 | return $this->str; 17 | } 18 | 19 | public function getStrToMatch() { 20 | return $this->strToMatch; 21 | } 22 | 23 | public function __toString() { 24 | return get_class($this->matcher).' - String not matched: expected ['.$this->strToMatch.'], actual ['.$this->str.']'; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /test/Exar/TestClasses/ClassWithConstructorParams.php: -------------------------------------------------------------------------------- 1 | param1 = $param1; 14 | $this->param2 = $param2; 15 | } 16 | 17 | public function getParam1() { 18 | return $this->param1; 19 | } 20 | 21 | public function getParam2() { 22 | return $this->param2; 23 | } 24 | 25 | public function getValue() { 26 | return $this->value; 27 | } 28 | 29 | /** 30 | * @PostConstruct 31 | */ 32 | public function init() { 33 | $this->value = 345; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /lib/Exar/Aop/AnnotationProcessor.php: -------------------------------------------------------------------------------- 1 | registerAnnotations($rClass->getAnnotations(true)); 11 | 12 | /* method annotations */ 13 | foreach ($rClass->getMethods() as $method) { 14 | InterceptorManager::getInstance()->registerAnnotations($method->getAnnotations(true)); 15 | } 16 | 17 | /* property annotations */ 18 | foreach ($rClass->getProperties() as $property) { 19 | InterceptorManager::getInstance()->registerAnnotations($property->getAnnotationMap()); 20 | } 21 | } 22 | 23 | } -------------------------------------------------------------------------------- /test/Exar/SimpleTest.php: -------------------------------------------------------------------------------- 1 | matchers as $matcher) { // loop over matchers, maintain matcher order 11 | $toParse = trim($toParse); // ignore whitespaces 12 | 13 | $value = $matcher->match($toParse); 14 | if (is_array($value)) { 15 | if ($matcher instanceof ValueMatcher) $result[] = $value; 16 | else $result = array_merge($result, $value); 17 | } else { 18 | $result[] = $value; 19 | } 20 | } 21 | 22 | $str = $toParse; 23 | return $result; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /lib/Exar/Annotation/Matcher/ConstantsMatcher.php: -------------------------------------------------------------------------------- 1 | true, 11 | 'TRUE' => true, 12 | 'false' => false, 13 | 'FALSE' => false, 14 | 'null' => null, 15 | 'NULL' => null 16 | ); 17 | } 18 | } 19 | 20 | public function match(&$str) { 21 | foreach (self::$mapping as $key => $value) { 22 | if (preg_match("/^$key/", $str)) { // value matched 23 | $str = substr($str, strlen($key)); 24 | return $value; 25 | } 26 | } 27 | throw new StringNotMatchedException($this, $str, ''); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /test/Exar/TestClasses/ExarAnnotatedClass.php: -------------------------------------------------------------------------------- 1 | assertEmpty($m->match($str)); 12 | $this->assertEquals('123)', $str); 13 | 14 | $str = 'text'; 15 | try { 16 | $m->match($str); 17 | $this->fail('An exception was expected'); 18 | } catch(StringNotMatchedException $e) { 19 | $this->assertEquals('text', $str); 20 | } 21 | } 22 | 23 | /** 24 | * @expectedException Exar\Annotation\Matcher\StringNotMatchedException 25 | */ 26 | public function testWhiteSpaces() { 27 | $m = new StringMatcher('('); 28 | $str = ' 456 '; 29 | $m->match($str); 30 | } 31 | 32 | /** 33 | * @expectedException Exar\Annotation\Matcher\StringNotMatchedException 34 | */ 35 | public function testStringTooLong() { 36 | $m = new StringMatcher('abcde'); 37 | $str = 'abcd'; 38 | $m->match($str); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /test/Exar/Annotation/Matcher/NumberMatcherTest.php: -------------------------------------------------------------------------------- 1 | match($str); 12 | $this->assertTrue(is_integer($result)); 13 | $this->assertEquals(34, $result); 14 | $this->assertEquals('abc', $str); 15 | 16 | $str = '3.14159-'; 17 | $result = $m->match($str); 18 | $this->assertTrue(is_float($result)); 19 | $this->assertEquals(3.14159, $result); 20 | $this->assertEquals('-', $str); 21 | 22 | $str = 'str'; 23 | try { 24 | $m->match($str); 25 | $this->fail('An exception was expected'); 26 | } catch(StringNotMatchedException $e) { 27 | $this->assertEquals('str', $str); 28 | } 29 | } 30 | 31 | /** 32 | * @expectedException Exar\Annotation\Matcher\StringNotMatchedException 33 | */ 34 | public function testWhiteSpaces() { 35 | $m = new NumberMatcher('('); 36 | $str = ' 654 '; 37 | $m->match($str); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /lib/Exar/Aop/Interceptor/Path.php: -------------------------------------------------------------------------------- 1 | getTargetClass()); 12 | $rMethod = $rClass->getMethod($this->getTargetMethod()); 13 | 14 | $errorCause = null; 15 | if (!$rMethod->isPublic()) { 16 | $errorCause = 'non-public method'; 17 | } 18 | 19 | if ($rMethod->isAbstract()) { 20 | $errorCause = 'abstract method'; 21 | } 22 | 23 | if ($rMethod->isStatic()) { 24 | $errorCause = 'static method'; 25 | } 26 | 27 | if ($rMethod->isConstructor()) { 28 | $errorCause = 'constructor'; 29 | } 30 | 31 | if ($rMethod->isDestructor()) { 32 | $errorCause = 'destructor'; 33 | } 34 | 35 | if ($errorCause !== null) { 36 | trigger_error('Annotation "'.get_class($this).'" is not allowed on '.$errorCause.' "'.$this->getTargetMethod().'"', E_USER_ERROR); 37 | } 38 | } 39 | 40 | public function getValue() { 41 | return $this->value; 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /lib/Exar/Annotation/Matcher/SeparatedValuesMatcher.php: -------------------------------------------------------------------------------- 1 | matcherForSeparatedValues = $matcherForSeparatedValues; 9 | } 10 | 11 | public function match(&$str) { 12 | $toParse = $str; 13 | $separatorMatcher = new StringMatcher(','); 14 | 15 | $result = array(); 16 | 17 | while (true) { 18 | $toParse = trim($toParse); 19 | 20 | try { 21 | $value = $this->matcherForSeparatedValues->match($toParse); 22 | if ($this->matcherForSeparatedValues instanceof ValueMatcher) { 23 | $result[] = $value; 24 | } else { 25 | $result = array_merge($result, $value); 26 | } 27 | } catch (StringNotMatchedException $e) { 28 | throw $e; 29 | } 30 | 31 | try { 32 | $toParse = trim($toParse); 33 | $separatorMatcher->match($toParse); 34 | } catch (StringNotMatchedException $e) { 35 | $str = $toParse; 36 | return $result; 37 | } 38 | } 39 | } 40 | } -------------------------------------------------------------------------------- /lib/Exar/Aop/InterceptorException.php: -------------------------------------------------------------------------------- 1 | 8 | * It is used to immediately stop interceptor actions and should only be used by generated code. 9 | */ 10 | class InterceptorException extends Exception { 11 | private $object; 12 | private $result; 13 | 14 | /** 15 | * Constructor. 16 | * 17 | * @param string $message exception message 18 | * @param object $obj object in which exception was thrown 19 | * @param mixed $result result to set 20 | */ 21 | public function __construct($message, $obj, $result = null) { 22 | parent::__construct($message); 23 | $this->object = $obj; 24 | $this->result = $result; 25 | } 26 | 27 | /** 28 | * Returns the object where the exception was thrown. 29 | * 30 | * @return object object that threw this exception 31 | */ 32 | public function getObject() { 33 | return $this->object; 34 | } 35 | 36 | /** 37 | * Result of the method execution where the exception was thrown. 38 | * 39 | * @return mixed|null result of the method execution or null if there is no result available 40 | */ 41 | public function getResult() { 42 | return $this->result; 43 | } 44 | } -------------------------------------------------------------------------------- /lib/Exar/Aop/Interceptor/PostConstruct.php: -------------------------------------------------------------------------------- 1 | setTargetMethod('__construct'); 18 | $this->setAllowValueProperty(false); 19 | } 20 | 21 | public function afterInvocation(InvocationContext $context, $result) { 22 | $rClass = new ReflectionClass($context->getClassName()); 23 | foreach ($rClass->getMethods() as $method) { 24 | if ($method->hasAnnotation($this->getName())) { 25 | $methodToCall = $method->getName(); 26 | $rMethod = $rClass->getMethod($methodToCall); 27 | parent::checkBooleanConstraint($rMethod->isPublic() && !$rMethod->isStatic() && count($rMethod->getParameters()) == 0); 28 | $context->getTarget()->$methodToCall(); 29 | } 30 | } 31 | return $result; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /test/Exar/Annotation/AnnotationParserTest.php: -------------------------------------------------------------------------------- 1 | readAnnotations($class->getDocComment(), $class); 15 | 16 | $this->assertEquals(3, count($annotations)); 17 | $this->assertEquals(array('Exar', 'CorrectAnnotation', 'AnotherCorrectAnnotation'), array_keys($annotations)); 18 | } 19 | 20 | public function testCustomAnnotations() { 21 | Autoloader::addAnnotationNamespaces('\Exar\TestClasses\Annotation'); 22 | $parser = AnnotationParser::getInstance(); 23 | 24 | $class = new ReflectionClass('\Exar\TestClasses\ClassWithCustomAnnotation'); 25 | $annotations = $parser->readAnnotations($class->getDocComment(), $class); 26 | 27 | $exarAnnotation = $annotations['Exar'][0]; 28 | $this->assertTrue($exarAnnotation instanceof SimpleAnnotation); 29 | 30 | $customAnnotation = $annotations['CustomAnnotation'][0]; 31 | $this->assertTrue($customAnnotation instanceof CustomAnnotation); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /lib/Exar/Reflection/ReflectionMethod.php: -------------------------------------------------------------------------------- 1 | annotationContainer = new AnnotationContainer($this); 18 | } 19 | 20 | /** 21 | * @see Exar\Reflection\ReflectionInterface::hasAnnotation() 22 | */ 23 | public function hasAnnotation($name) { 24 | return $this->annotationContainer->hasAnnotation($name); 25 | } 26 | 27 | /** 28 | * @see Exar\Reflection\ReflectionInterface::getAnnotation() 29 | */ 30 | public function getAnnotation($name) { 31 | return $this->annotationContainer->getAnnotation($name); 32 | } 33 | 34 | /** 35 | * @see Exar\Reflection\ReflectionInterface::getAnnotationMap() 36 | */ 37 | public function getAnnotationMap() { 38 | return $this->annotationContainer->getAnnotationMap(); 39 | } 40 | 41 | /** 42 | * @see Exar\Reflection\ReflectionInterface::getAnnotations() 43 | */ 44 | public function getAnnotations($considerMultipleTag = false) { 45 | return $this->annotationContainer->getAnnotations($considerMultipleTag); 46 | } 47 | 48 | } -------------------------------------------------------------------------------- /lib/Exar/Reflection/ReflectionProperty.php: -------------------------------------------------------------------------------- 1 | annotationContainer = new AnnotationContainer($this); 18 | } 19 | 20 | /** 21 | * @see Exar\Reflection\ReflectionInterface::hasAnnotation() 22 | */ 23 | public function hasAnnotation($name) { 24 | return $this->annotationContainer->hasAnnotation($name); 25 | } 26 | 27 | /** 28 | * @see Exar\Reflection\ReflectionInterface::getAnnotation() 29 | */ 30 | public function getAnnotation($name) { 31 | return $this->annotationContainer->getAnnotation($name); 32 | } 33 | 34 | /** 35 | * @see Exar\Reflection\ReflectionInterface::getAnnotationMap() 36 | */ 37 | public function getAnnotationMap() { 38 | return $this->annotationContainer->getAnnotationMap(); 39 | } 40 | 41 | /** 42 | * @see Exar\Reflection\ReflectionInterface::getAnnotations() 43 | */ 44 | public function getAnnotations($considerMultipleTag = false) { 45 | return $this->annotationContainer->getAnnotations($considerMultipleTag); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /lib/Exar/Aop/Interceptor/Track.php: -------------------------------------------------------------------------------- 1 | createMessage($context, 'Before invocation'); 18 | } 19 | 20 | public function afterReturning(InvocationContext $context, $result) { 21 | echo $this->createMessage($context, 'After returning'); 22 | return $result; 23 | } 24 | 25 | public function afterThrowing(InvocationContext $context) { 26 | echo $this->createMessage($context, 'After throwing'); 27 | } 28 | 29 | public function afterInvocation(InvocationContext $context, $result) { 30 | echo $this->createMessage($context, 'After invocation'); 31 | return $result; 32 | } 33 | 34 | private function createMessage(InvocationContext $context, $prefix = '') { 35 | if ($this->value === null) { 36 | return $prefix.': '.$context->getClassName().'->'.$context->getMethodName().' ('.date('d.m.Y H:i:s', time()).')'.PHP_EOL; 37 | } 38 | return $this->value; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /test/Exar/Annotation/Matcher/ArrayMatcherTest.php: -------------------------------------------------------------------------------- 1 | assertEmpty($m->match($str)); 12 | 13 | $str = '{123}'; // simple array with one element 14 | $this->assertEquals(array(123), $m->match($str)); 15 | 16 | $str = ' { 345 } '; // white spaces 17 | $this->assertEquals(array(345), $m->match($str)); 18 | 19 | $str = '{1,3,5}'; // multiple values 20 | $this->assertEquals(array(1, 3, 5), $m->match($str)); 21 | 22 | $str = '{"a", \'b\', 3.14, 1000, NULL, false}'; // different value types 23 | $this->assertEquals(array('a', 'b', 3.14, 1000, null, false), $m->match($str)); 24 | 25 | $str = '{1, { 2, 3, 4 }, 5}'; // nested arrays 26 | $this->assertEquals(array(1, array(2, 3, 4), 5), $m->match($str)); 27 | 28 | $str = '{6, {}, 7}'; // nested arrays 29 | $this->assertEquals(array(6, array(), 7), $m->match($str)); 30 | } 31 | 32 | /** 33 | * @expectedException Exar\Annotation\Matcher\StringNotMatchedException 34 | */ 35 | public function testAssocArray() { 36 | $m = new ArrayMatcher(); 37 | $str = '{"a" => 23}'; 38 | $m->match($str); 39 | } 40 | 41 | /** 42 | * @expectedException Exar\Annotation\Matcher\StringNotMatchedException 43 | */ 44 | public function testBadArrayValue() { 45 | $m = new ArrayMatcher(); 46 | $str = '{1,}'; 47 | $m->match($str); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /lib/Exar/Reflection/ReflectionInterface.php: -------------------------------------------------------------------------------- 1 | add(new RegexMatcher(self::REGEX_ANNOTATION_NAME)); // annotation name at the beginning 13 | 14 | $parametersMatcher = new ParallelMatcher(); 15 | 16 | $valuesMatcher = new SequentialMatcher(); 17 | $valuesMatcher->add(new StringMatcher('(')); 18 | 19 | $pMatcher = new ParallelMatcher(); 20 | $pMatcher->add(new SeparatedValuesMatcher(new KeyValuePairMatcher())); // for separated parameters 21 | $pMatcher->add(new SingleValueMatcher()); // for top level parameter 22 | $pMatcher->add(new StringMatcher()); // for empty parameter 23 | $valuesMatcher->add($pMatcher); 24 | 25 | $valuesMatcher->add(new StringMatcher(')')); 26 | 27 | $parametersMatcher->add($valuesMatcher); 28 | $parametersMatcher->add(new StringMatcher()); // for annotations without parameters 29 | 30 | $this->add($parametersMatcher); 31 | } 32 | 33 | public function match(&$str) { 34 | $strBackup = $str; 35 | $result = parent::match($str); 36 | 37 | $str = trim($str); 38 | if ($str != '' && $str != '*/') { 39 | trigger_error('Wrong annotation syntax: '.$strBackup.'; Could not parse string: '.$str, E_USER_ERROR); 40 | } 41 | 42 | $annotationName = array_shift($result); 43 | 44 | return array( 45 | 'name' => $annotationName, 46 | 'parameters' => $result 47 | ); 48 | } 49 | } -------------------------------------------------------------------------------- /test/Exar/Reflection/ReflectionClassTest.php: -------------------------------------------------------------------------------- 1 | getAnnotations(); 14 | 15 | $this->assertEquals(6, count($annotations)); 16 | 17 | $noValue = $annotations[0]; 18 | $this->assertTrue($noValue instanceof \Exar\Annotation\SimpleAnnotation); 19 | $this->assertEquals('NoValue', $noValue->getName()); 20 | $this->assertNull($noValue->getValue()); 21 | 22 | $floatValue = $annotations[1]; 23 | $this->assertTrue($floatValue instanceof \Exar\Annotation\SimpleAnnotation); 24 | $this->assertEquals('FloatValue', $floatValue->getName()); 25 | $this->assertEquals(1.5, $floatValue->getValue()); 26 | 27 | $stringValue = $annotations[2]; 28 | $this->assertTrue($stringValue instanceof \Exar\Annotation\SimpleAnnotation); 29 | $this->assertEquals('StringValue', $stringValue->getName()); 30 | $this->assertEquals('string value', $stringValue->getValue()); 31 | 32 | $arrayValue = $annotations[3]; 33 | $this->assertTrue($arrayValue instanceof \Exar\Annotation\SimpleAnnotation); 34 | $this->assertEquals('ArrayValue', $arrayValue->getName()); 35 | $this->assertEquals(array(1, 2, 3), $arrayValue->getValue()); 36 | 37 | $arrayValue2 = $annotations[4]; 38 | $this->assertTrue($arrayValue2 instanceof \Exar\Annotation\SimpleAnnotation); 39 | $this->assertEquals('ArrayValue', $arrayValue2->getName()); 40 | $this->assertEquals(array('a', 'b', 'c'), $arrayValue2->getValue()); 41 | 42 | $withNamespace = $annotations[5]; 43 | $this->assertTrue($withNamespace instanceof \Exar\Annotation\SimpleAnnotation); 44 | $this->assertEquals('Annotation\With\Namespace', $withNamespace->getName()); 45 | $this->assertEquals(array('a', true, null), $withNamespace->getValue()); 46 | 47 | $this->assertEquals($noValue, $class->getAnnotation('NoValue')); 48 | $this->assertEquals($arrayValue, $class->getAnnotation('ArrayValue')); 49 | } 50 | 51 | } 52 | -------------------------------------------------------------------------------- /lib/Exar/Reflection/AnnotationContainer.php: -------------------------------------------------------------------------------- 1 | getDocComment(); 22 | 23 | if ($comment != '') { 24 | $this->annotations = AnnotationParser::getInstance()->readAnnotations($comment, $reflectionObject); 25 | } 26 | } 27 | 28 | /** 29 | * @see Exar\Reflection\ReflectionInterface::hasAnnotation() 30 | */ 31 | public function hasAnnotation($name) { 32 | return isset($this->annotations[$name]); 33 | } 34 | 35 | /** 36 | * @see Exar\Reflection\ReflectionInterface::getAnnotation() 37 | */ 38 | public function getAnnotation($name) { 39 | return ($this->hasAnnotation($name)) ? $this->annotations[$name][0] : null; 40 | } 41 | 42 | /** 43 | * @see Exar\Reflection\ReflectionInterface::getAnnotationMap() 44 | */ 45 | public function getAnnotationMap() { 46 | return $this->annotations; 47 | } 48 | 49 | /** 50 | * @see Exar\Reflection\ReflectionInterface::getAnnotations() 51 | */ 52 | public function getAnnotations($considerMultipleTag = false) { 53 | $arr = array(); 54 | foreach($this->annotations as $name => $annotationArr) { 55 | if ($considerMultipleTag) { // Non-multiple annotations will be returned only once 56 | $annotation = $annotationArr[0]; 57 | $classReflection = new ReflectionClass(get_class($annotation)); 58 | if ($classReflection->hasAnnotation('Multiple')) { // Multiple annotations are allowed 59 | $arr = array_merge($arr, $annotationArr); 60 | } else { 61 | $arr[] = $annotation; 62 | } 63 | } else { 64 | $arr = array_merge($arr, $annotationArr); 65 | } 66 | } 67 | return $arr; 68 | } 69 | 70 | } -------------------------------------------------------------------------------- /test/Exar/Aop/Interceptor/PostConstructTest.php: -------------------------------------------------------------------------------- 1 | assertEquals(123, $obj->getValue()); 19 | } 20 | 21 | public function testWithoutConstructor() { 22 | $obj = new ClassWithoutConstructor(); 23 | 24 | $class = new ReflectionClass('\Exar\TestClasses\ClassWithoutConstructor'); 25 | $this->assertTrue($class->hasMethod('__construct')); 26 | 27 | // the value is set within the PostConstruct annotated method 28 | $this->assertEquals(345, $obj->getValue()); 29 | } 30 | 31 | public function testWithConstructorParams() { 32 | $obj = new ClassWithConstructorParams('foo', 'bar'); 33 | 34 | $class = new ReflectionClass('\Exar\TestClasses\ClassWithoutConstructor'); 35 | $this->assertTrue($class->hasMethod('__construct')); 36 | 37 | $this->assertEquals('foo', $obj->getParam1()); 38 | $this->assertEquals('bar', $obj->getParam2()); 39 | 40 | // the value is set within the PostConstruct annotated method 41 | $this->assertEquals(345, $obj->getValue()); 42 | } 43 | 44 | public function testWithParentConstructor() { 45 | $obj = new ClassWithParentConstructor('foo', 'bar'); 46 | 47 | $class = new ReflectionClass('\Exar\TestClasses\ClassWithoutConstructor'); 48 | $this->assertTrue($class->hasMethod('__construct')); 49 | 50 | $this->assertEquals('foo', $obj->getParam1()); 51 | $this->assertEquals('bar', $obj->getParam2()); 52 | 53 | // the value is set within the PostConstruct annotated method of the parent class 54 | $this->assertEquals(345, $obj->getValue()); 55 | 56 | // the value is set within the PostConstruct annotated method 57 | $this->assertEquals(789, $obj->getAnotherValue()); 58 | } 59 | 60 | public function testWithParentsParentConstructor() { 61 | $obj = new ClassWithParentsParentConstructor('bar', 'baz'); 62 | 63 | $class = new ReflectionClass('\Exar\TestClasses\ClassWithoutConstructor'); 64 | $this->assertTrue($class->hasMethod('__construct')); 65 | 66 | $this->assertEquals('bar', $obj->getParam1()); 67 | $this->assertEquals('baz', $obj->getParam2()); 68 | 69 | // the value is set within the PostConstruct annotated method of the parent's parent class 70 | $this->assertEquals(345, $obj->getValue()); 71 | 72 | // the value is set within the PostConstruct annotated method 73 | $this->assertEquals(654, $obj->getAnotherValue()); 74 | } 75 | 76 | } 77 | -------------------------------------------------------------------------------- /lib/Exar/Reflection/ReflectionClass.php: -------------------------------------------------------------------------------- 1 | annotationContainer = new AnnotationContainer($this); 18 | } 19 | 20 | /** 21 | * @see Exar\Reflection\ReflectionInterface::hasAnnotation() 22 | */ 23 | public function hasAnnotation($name) { 24 | return $this->annotationContainer->hasAnnotation($name); 25 | } 26 | 27 | /** 28 | * @see Exar\Reflection\ReflectionInterface::getAnnotation() 29 | */ 30 | public function getAnnotation($name) { 31 | return $this->annotationContainer->getAnnotation($name); 32 | } 33 | 34 | /** 35 | * @see Exar\Reflection\ReflectionInterface::getAnnotationMap() 36 | */ 37 | public function getAnnotationMap() { 38 | return $this->annotationContainer->getAnnotationMap(); 39 | } 40 | 41 | /** 42 | * @see Exar\Reflection\ReflectionInterface::getAnnotations() 43 | */ 44 | public function getAnnotations($considerMultipleFlag = false) { 45 | return $this->annotationContainer->getAnnotations($considerMultipleFlag); 46 | } 47 | 48 | /** 49 | * Returns a ReflectionMethod object for a class method. 50 | * 51 | * @param string $name the method name to reflect 52 | * @return ReflectionMethod a ReflectionMethod object providing the extended Reflection API 53 | */ 54 | public function getMethod($name) { 55 | return new ReflectionMethod($this->getName(), $name); 56 | } 57 | 58 | /** 59 | * Gets an array of methods. 60 | * 61 | * @param int $filter filter the results to include only methods with certain attributes 62 | * @return array array of ReflectionMethod objects 63 | */ 64 | public function getMethods($filter = -1) { 65 | $arr = array(); 66 | foreach(parent::getMethods($filter) as $method) { 67 | $arr[] = new ReflectionMethod($this->getName(), $method->getName()); 68 | } 69 | return $arr; 70 | } 71 | 72 | /** 73 | * Returns a ReflectionProperty for a class's property. 74 | * 75 | * @param string $name the property name to reflect 76 | * @return ReflectionProperty a ReflectionProperty object providing the extended Reflection API 77 | */ 78 | public function getProperty($name) { 79 | return new ReflectionProperty($this->getName(), $name); 80 | } 81 | 82 | /** 83 | * Gets an array of properties. 84 | * 85 | * @param int $filter filter the results to include only methods with certain attributes 86 | * @return array array of ReflectionProperty objects 87 | */ 88 | public function getProperties($filter = -1) { 89 | $arr = array(); 90 | foreach(parent::getProperties($filter) as $property) { 91 | if ($property->getDeclaringClass()->getName() == $this->getName()) { 92 | $arr[] = new ReflectionProperty($this->getName(), $property->getName()); 93 | } 94 | } 95 | return $arr; 96 | } 97 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | exar-framework 2 | ============== 3 | A lightweight AOP layer for PHP. 4 | 5 | Installation 6 | ------------ 7 | The simplest way to use Exar is to install it via Composer. 8 | 9 | Create a `composer.json` file in your project root and define the dependency: 10 | 11 | { 12 | "require": { 13 | "techdev-solutions/exar": "dev-master" 14 | }, 15 | "minimum-stability": "dev" 16 | } 17 | 18 | Install Composer in your project: 19 | 20 | curl -s http://getcomposer.org/installer | php 21 | 22 | 23 | Tell Composer to download and install the dependencies: 24 | 25 | php composer.phar install 26 | 27 | Now you are ready to code with Exar! 28 | 29 | Creating a simple PHP application using Exar 30 | -------------------------------------------- 31 | 32 | Create a package with a PHP class (e.g. `/lib/MyProject/Person.php`) that will become AOP features provided by Exar: 33 | 34 | namespace MyProject; 35 | 36 | /** 37 | * @Exar 38 | */ 39 | class Person { 40 | private $firstName; 41 | private $lastName; 42 | 43 | public function __construct($firstName, $lastName) { 44 | $this->firstName = $firstName; 45 | $this->lastName = $lastName; 46 | } 47 | 48 | /** 49 | * @Track 50 | */ 51 | public function setFirstName($firstName) { 52 | $this->firstName = $firstName; 53 | } 54 | 55 | public function getFirstName() { 56 | return $this->firstName; 57 | } 58 | 59 | public function getLastName() { 60 | return $this->lastName; 61 | } 62 | } 63 | 64 | 65 | Create `index.php` file in the project root which will be the main file of your application: 66 | 67 | /** load Composer dependencies */ 68 | require_once 'vendor/autoload.php'; 69 | 70 | /** add your class directory (where MyProject/Person.php is) to the include path */ 71 | set_include_path(dirname(__FILE__) . '/lib/' . PATH_SEPARATOR . get_include_path()); 72 | 73 | /** register namespaces that will be loaded by Exar (the namespace of Person.php) */ 74 | Exar\Autoloader::register(dirname(__FILE__) . '/_cache', array('MyProject')); 75 | 76 | $person = new MyProject\Person('John', 'Smith'); 77 | echo 'first name = '.$person->getFirstName() . PHP_EOL; 78 | echo 'last name = '.$person->getLastName() . PHP_EOL; 79 | 80 | $person->setFirstName('Jim'); 81 | echo 'first name = '.$person->getFirstName() . PHP_EOL; 82 | echo 'last name = '.$person->getLastName() . PHP_EOL; 83 | 84 | 85 | Now run `index.php` and see the console output: 86 | 87 | first name = John 88 | last name = Smith 89 | Before invocation: MyProject\Person->setFirstName (03.07.2014 11:45:48) 90 | After returning: MyProject\Person->setFirstName (03.07.2014 11:45:48) 91 | After invocation: MyProject\Person->setFirstName (03.07.2014 11:45:48) 92 | first name = Jim 93 | last name = Smith 94 | 95 | 96 | What happened? 97 | 98 | You created a `Person` object and printed the first and the last name. After that, you set the first name again. 99 | Since the method `setFirstName` is annotated with `@Track`, Exar intercepts the method execution and invokes the correspondent interceptor code. 100 | In this case, `@Track` just echoes the class and the name of the intercepted method, with the current timestamp. 101 | This example shows how Exar works: It adds functionality to your PHP classes on the basis of annotations within docblocks. 102 | 103 | Stay tuned for more docs and examples! 104 | -------------------------------------------------------------------------------- /test/Exar/Reflection/AnnotationContainerTest.php: -------------------------------------------------------------------------------- 1 | assertTrue(self::$container->hasAnnotation('One')); 18 | $this->assertTrue(self::$container->hasAnnotation('Two')); 19 | $this->assertTrue(self::$container->hasAnnotation('Three')); 20 | $this->assertTrue(self::$container->hasAnnotation('MultipleAnnotation')); 21 | $this->assertFalse(self::$container->hasAnnotation('Four')); 22 | } 23 | 24 | public function testGetAnnotation() { 25 | $this->assertEquals('i', self::$container->getAnnotation('One')->getValue()); 26 | $this->assertEquals('g', self::$container->getAnnotation('Two')->getValue()); 27 | $this->assertEquals('c', self::$container->getAnnotation('Three')->getValue()); 28 | $this->assertEquals('h', self::$container->getAnnotation('MultipleAnnotation')->getValue()); 29 | $this->assertNull(self::$container->getAnnotation('Four')); 30 | } 31 | 32 | public function testGetAnnotationsWithMultipleTag() { 33 | $annotations = self::$container->getAnnotations(true); 34 | 35 | $this->assertEquals(6, count($annotations)); 36 | 37 | $this->assertEquals('i', $annotations[0]->getValue()); 38 | $this->assertEquals('g', $annotations[1]->getValue()); 39 | $this->assertEquals('c', $annotations[2]->getValue()); 40 | $this->assertEquals('h', $annotations[3]->getValue()); 41 | $this->assertEquals('f', $annotations[4]->getValue()); 42 | $this->assertEquals('d', $annotations[5]->getValue()); 43 | } 44 | 45 | public function testGetAnnotationsWithoutMultipleTag() { 46 | $annotations = self::$container->getAnnotations(false); 47 | 48 | $this->assertEquals(9, count($annotations)); 49 | 50 | $this->assertEquals('i', $annotations[0]->getValue()); 51 | $this->assertEquals('e', $annotations[1]->getValue()); 52 | $this->assertEquals('a', $annotations[2]->getValue()); 53 | $this->assertEquals('g', $annotations[3]->getValue()); 54 | $this->assertEquals('b', $annotations[4]->getValue()); 55 | $this->assertEquals('c', $annotations[5]->getValue()); 56 | $this->assertEquals('h', $annotations[6]->getValue()); 57 | $this->assertEquals('f', $annotations[7]->getValue()); 58 | $this->assertEquals('d', $annotations[8]->getValue()); 59 | } 60 | 61 | public function testGetAnnotationMap() { 62 | $arr = self::$container->getAnnotationMap(false); 63 | 64 | $this->assertEquals(4, count($arr)); 65 | 66 | $this->assertTrue(array_key_exists('One', $arr)); 67 | $this->assertTrue(array_key_exists('Two', $arr)); 68 | $this->assertTrue(array_key_exists('Three', $arr)); 69 | $this->assertTrue(array_key_exists('MultipleAnnotation', $arr)); 70 | $this->assertFalse(array_key_exists('Four', $arr)); 71 | 72 | $this->assertEquals(3, count($arr['One'])); 73 | $this->assertEquals(2, count($arr['Two'])); 74 | $this->assertEquals(1, count($arr['Three'])); 75 | $this->assertEquals(3, count($arr['MultipleAnnotation'])); 76 | } 77 | 78 | } 79 | -------------------------------------------------------------------------------- /lib/Exar/Aop/InvocationContext.php: -------------------------------------------------------------------------------- 1 | 6 | * This class should only be used by generated code. 7 | */ 8 | class InvocationContext { 9 | private $target; // target object 10 | private $className; // target object's type 11 | private $methodName; // name of the method to intercept 12 | private $params; // parameters of the intercepted method 13 | private $exception = null; // exception thrown during method invocation 14 | 15 | /** 16 | * Constructor. 17 | * 18 | * @param $target target object 19 | * @param $className target object's type 20 | * @param $methodName name of the method to intercept 21 | * @param array $params parameters of the intercepted method 22 | */ 23 | public function __construct($target, $className, $methodName, array $params) { 24 | $this->target = $target; 25 | $this->className = $className; 26 | $this->methodName = $methodName; 27 | $this->params = $params; 28 | } 29 | 30 | /** 31 | * Return the target object. 32 | * 33 | * @return target object 34 | */ 35 | public function getTarget() { 36 | return $this->target; 37 | } 38 | 39 | /** 40 | * Sets a method parameter. This method can be used for subsequently manipulations on the method parameters. 41 | * 42 | * @param $name parameter name 43 | * @param $value parameter value 44 | */ 45 | public function setParam($name, $value) { 46 | $this->params[$name] = $value; 47 | } 48 | 49 | /** 50 | * Overrides all method parameters. This method can be used for subsequently manipulations on the method parameters. 51 | * 52 | * @param array $params method parameters 53 | */ 54 | public function setParams(array $params) { 55 | $this->params = $params; 56 | } 57 | 58 | /** 59 | * Returns the parameters of the intercepted method. 60 | * 61 | * @return array method parameters 62 | */ 63 | public function getParams() { 64 | return $this->params; 65 | } 66 | 67 | /** 68 | * Returns the type of the target object. 69 | * 70 | * @return target target object's type 71 | */ 72 | public function getClassName() { 73 | return $this->className; 74 | } 75 | 76 | /** 77 | * Return the name of the intercepted method. 78 | * 79 | * @return name method name 80 | */ 81 | public function getMethodName() { 82 | return $this->methodName; 83 | } 84 | 85 | /** 86 | * Sets the exception object. 87 | * 88 | * @param \Exception $e exception object 89 | */ 90 | public function setException(\Exception $e) { 91 | $this->exception = $e; 92 | } 93 | 94 | /** 95 | * Returns the exception thrown during the method invocation (if any available). 96 | * 97 | * @return \Exception exception object or null if no exception was thrown 98 | */ 99 | public function getException() { 100 | return $this->exception; 101 | } 102 | 103 | /** 104 | * Removes the exception from invocation context. (Can be used to manipulate interception process in a deeper way). 105 | */ 106 | public function deleteException() { 107 | $this->exception = null; 108 | } 109 | 110 | /** 111 | * Returns true if an exception was thrown during method invocation, else false. 112 | * 113 | * @return bool true if an exception was thrown during method invocation, else false 114 | */ 115 | public function hasException() { 116 | return $this->exception !== null; 117 | } 118 | 119 | } -------------------------------------------------------------------------------- /lib/Exar/Annotation/AnnotationParser.php: -------------------------------------------------------------------------------- 1 | match($line); // annotation data 37 | 38 | if ($arr['name'] !== null) { 39 | $annotationName = $arr['name']; 40 | } else { 41 | $arr['name'] = $annotationName; // annotation without parameters 42 | } 43 | 44 | $parameters = $arr['parameters']; // get annotation parameters 45 | 46 | if (!isset($annotations[$annotationName])) { 47 | $annotations[$annotationName] = array(); // initialize annotation array (every target can contain several annotations with the same name) 48 | } 49 | 50 | $annotationInstantiated = false; // initial value - annotation object is not instantiated yet 51 | 52 | foreach (Autoloader::getAnnotationNamespaces() as $namespace) { // walk through registered annotation namespaces 53 | try { 54 | $className = $namespace.'\\'.$annotationName; // build class name for the annotation object 55 | 56 | if (!in_array($className, get_declared_classes())) { // class is not declared yet 57 | if (!\Exar\Autoloader::autoload($className)) { // class definition not found 58 | continue; // do nothing, jump to the next registered annotation namespaces 59 | } 60 | } 61 | 62 | $rAnnotation = new \ReflectionClass($className); // create reflection object of the annotation class 63 | $annotation = $rAnnotation->newInstance($parameters, $targetReflection); // instantiate annotation object 64 | array_unshift($annotations[$annotationName], $annotation); // remember created annotation object in an array 65 | $annotationInstantiated = true; // set instantiation flag to "true" 66 | break; // since the annotation object is created, we don't need to check other namespaces and can leave the loop 67 | } catch (\ReflectionException $e) { 68 | // There was no annotation found within the current namespace 69 | } 70 | } 71 | 72 | if (!$annotationInstantiated) { // no annotation instantiated, so create an simple annotation object 73 | $simpleAnnotation = new SimpleAnnotation($parameters, $targetReflection, $annotationName); 74 | array_unshift($annotations[$annotationName], $simpleAnnotation); 75 | } 76 | } 77 | return $annotations; 78 | } 79 | } -------------------------------------------------------------------------------- /lib/Exar/Autoloader.php: -------------------------------------------------------------------------------- 1 | process($file); 60 | } 61 | } 62 | } 63 | 64 | /** 65 | * Returns the weaver object. 66 | * 67 | * @return Aop\Weaver the weaver object (will be instantiated on the first method call) 68 | */ 69 | static private function getWeaver() { 70 | if (self::$weaver === null) { // the weaver is not yet instantiated 71 | self::$weaver = new \Exar\Aop\Weaver(self::$cacheDir); 72 | } 73 | return self::$weaver; 74 | } 75 | 76 | /** 77 | * Returns the cache directory of this autoloader. 78 | * 79 | * @return string the path ot the cache directory 80 | */ 81 | static public function getCacheDir() { 82 | return self::$cacheDir; 83 | } 84 | 85 | /** 86 | * Returns all registered namespaces. 87 | * 88 | * @return array registered namespaces 89 | */ 90 | static public function getNamespaces() { 91 | return self::$namespaces; 92 | } 93 | 94 | /** 95 | * Adds annotation namespaces. 96 | * 97 | * @param $ns a namespace or an array of namespaces to add 98 | */ 99 | static public function addAnnotationNamespaces($ns) { 100 | if (!is_array($ns)) { 101 | $ns = array($ns); 102 | } 103 | self::$annotationNamespaces = array_merge(self::$annotationNamespaces, $ns); 104 | } 105 | 106 | /** 107 | * Returns all registered annotation namespaces. 108 | * 109 | * @return array annotation namespaces 110 | */ 111 | static public function getAnnotationNamespaces() { 112 | return self::$annotationNamespaces; 113 | } 114 | 115 | /** 116 | * Deletes all files within the cache directory. 117 | */ 118 | static public function cleanCache() { 119 | array_map('unlink', glob(self::$cacheDir . '/*')); 120 | } 121 | 122 | } -------------------------------------------------------------------------------- /test/Exar/WeaverTest.php: -------------------------------------------------------------------------------- 1 | assertEquals(self::$cacheDir, $weaver->getCacheDir()); 18 | } 19 | 20 | public function testCacheDirDoesNotExist() { 21 | try { 22 | $weaver = new Weaver(dirname(__DIR__) . DIRECTORY_SEPARATOR . '_folder_does_not_exist'); 23 | } catch(\InvalidArgumentException $e) { 24 | $this->assertStringStartsWith('Cache directory does not exist: ', $e->getMessage()); 25 | } 26 | } 27 | 28 | public function testCacheDirNotADir() { 29 | try { 30 | $weaver = new Weaver(__FILE__); 31 | } catch(\InvalidArgumentException $e) { 32 | $this->assertStringStartsWith('Cache directory is not a directory: ', $e->getMessage()); 33 | } 34 | } 35 | 36 | public function testWeaverFileDoesNotExist() { 37 | $weaver = new Weaver(self::$cacheDir); 38 | try { 39 | $weaver->process(__FILE__ . '.does.not.exist'); 40 | } catch(\InvalidArgumentException $e) { 41 | $this->assertStringStartsWith('File does not exist: ', $e->getMessage()); 42 | } 43 | } 44 | 45 | public function testWeaverNoExarClass() { 46 | $weaver = new Weaver(self::$cacheDir); 47 | $weaver->process(__DIR__ . '/TestClasses/NoExarAnnotatedClass.php'); 48 | 49 | $this->assertEquals(0, count(glob(self::$cacheDir . '/NoExarAnnotatedClass.php*'))); 50 | } 51 | 52 | public function testWeaver() { 53 | $weaver = new Weaver(self::$cacheDir); 54 | 55 | $file = __DIR__ . '/TestClasses/ExarAnnotatedClass.php'; 56 | $weaver->process($file); 57 | 58 | $this->assertEquals(1, count(glob(self::$cacheDir . '/ExarAnnotatedClass___' . md5($file) . '.php'))); 59 | 60 | $rClass = new \ReflectionClass('Exar\TestClasses\ExarAnnotatedClass'); 61 | 62 | $this->assertEquals(17, count($rClass->getMethods())); // 17 methods are expecting, including 2 methods which handle object construction 63 | 64 | /** public */ 65 | $this->assertTrue($rClass->hasMethod('publicMethod')); 66 | $this->assertTrue(strpos($rClass->getMethod('publicMethod')->getDocComment(), '@A') !== false); 67 | $this->assertTrue($rClass->hasMethod('publicMethod' . Weaver::METHOD_NAME_SUFFIX)); 68 | $this->assertFalse($rClass->getMethod('publicMethod' . Weaver::METHOD_NAME_SUFFIX)->getDocComment()); 69 | 70 | /** public final */ 71 | $this->assertTrue($rClass->hasMethod('publicFinalMethod')); 72 | $this->assertTrue(strpos($rClass->getMethod('publicFinalMethod')->getDocComment(), '@B') !== false); 73 | $this->assertTrue($rClass->hasMethod('publicFinalMethod' . Weaver::METHOD_NAME_SUFFIX)); 74 | $this->assertFalse($rClass->getMethod('publicFinalMethod' . Weaver::METHOD_NAME_SUFFIX)->getDocComment()); 75 | 76 | /** public static */ 77 | $this->assertTrue($rClass->hasMethod('publicStaticMethod')); 78 | $this->assertTrue(strpos($rClass->getMethod('publicStaticMethod')->getDocComment(), '@C') !== false); 79 | $this->assertFalse($rClass->hasMethod('publicStaticMethod' . Weaver::METHOD_NAME_SUFFIX)); // no static methods are wrapped 80 | 81 | /** protected */ 82 | $this->assertTrue($rClass->hasMethod('protectedMethod')); 83 | $this->assertTrue(strpos($rClass->getMethod('protectedMethod')->getDocComment(), '@D') !== false); 84 | $this->assertTrue($rClass->hasMethod('protectedMethod' . Weaver::METHOD_NAME_SUFFIX)); 85 | $this->assertFalse($rClass->getMethod('protectedMethod' . Weaver::METHOD_NAME_SUFFIX)->getDocComment()); 86 | 87 | /** protected final */ 88 | $this->assertTrue($rClass->hasMethod('protectedFinalMethod')); 89 | $this->assertTrue(strpos($rClass->getMethod('protectedFinalMethod')->getDocComment(), '@E') !== false); 90 | $this->assertTrue($rClass->hasMethod('protectedFinalMethod' . Weaver::METHOD_NAME_SUFFIX)); 91 | $this->assertFalse($rClass->getMethod('protectedFinalMethod' . Weaver::METHOD_NAME_SUFFIX)->getDocComment()); 92 | 93 | /** protected static */ 94 | $this->assertTrue($rClass->hasMethod('protectedStaticMethod')); 95 | $this->assertTrue(strpos($rClass->getMethod('protectedStaticMethod')->getDocComment(), '@F') !== false); 96 | $this->assertFalse($rClass->hasMethod('protectedStaticMethod' . Weaver::METHOD_NAME_SUFFIX)); // no static methods are wrapped 97 | 98 | /** private */ 99 | $this->assertTrue($rClass->hasMethod('privateMethod')); 100 | $this->assertTrue(strpos($rClass->getMethod('privateMethod')->getDocComment(), '@G') !== false); 101 | $this->assertTrue($rClass->hasMethod('privateMethod' . Weaver::METHOD_NAME_SUFFIX)); 102 | $this->assertFalse($rClass->getMethod('privateMethod' . Weaver::METHOD_NAME_SUFFIX)->getDocComment()); 103 | 104 | /** private final */ 105 | $this->assertTrue($rClass->hasMethod('privateFinalMethod')); 106 | $this->assertTrue(strpos($rClass->getMethod('privateFinalMethod')->getDocComment(), '@H') !== false); 107 | $this->assertTrue($rClass->hasMethod('privateFinalMethod' . Weaver::METHOD_NAME_SUFFIX)); 108 | $this->assertFalse($rClass->getMethod('privateFinalMethod' . Weaver::METHOD_NAME_SUFFIX)->getDocComment()); 109 | 110 | /** private static */ 111 | $this->assertTrue($rClass->hasMethod('privateStaticMethod')); 112 | $this->assertTrue(strpos($rClass->getMethod('privateStaticMethod')->getDocComment(), '@I') !== false); 113 | $this->assertFalse($rClass->hasMethod('privateStaticMethod' . Weaver::METHOD_NAME_SUFFIX)); // no static methods are wrapped 114 | } 115 | 116 | } 117 | -------------------------------------------------------------------------------- /lib/Exar/Aop/InterceptorManager.php: -------------------------------------------------------------------------------- 1 | getTargetClass(); 45 | $methodName = $annotation->getTargetMethod(); 46 | 47 | if (!isset($this->annotations[$className])) { 48 | $this->annotations[$className] = array(); 49 | } 50 | 51 | if (!isset($this->annotations[$className][$methodName])) { 52 | $this->annotations[$className][$methodName] = array(); 53 | } 54 | 55 | $this->annotations[$className][$methodName][] = $annotation; 56 | } 57 | 58 | /** 59 | * Executes registered before advices (before the advised method is invoked). 60 | * 61 | * @param InvocationContext $context invocation context 62 | */ 63 | public function before(InvocationContext $context) { 64 | foreach ($this->detectInterceptorsForContext($context) as $interceptor) { 65 | if ($interceptor instanceof BeforeInvocationInterceptor) { 66 | $interceptor->beforeInvocation($context); 67 | } 68 | } 69 | } 70 | 71 | /** 72 | * Executes registered after throwing advices (after the advised method throws an exception during execution). 73 | * 74 | * @param InvocationContext $context invocation context 75 | */ 76 | public function afterThrowing(InvocationContext $context) { 77 | foreach ($this->detectInterceptorsForContext($context) as $interceptor) { 78 | if ($interceptor instanceof AfterThrowingInterceptor) { 79 | $interceptor->afterThrowing($context); 80 | } 81 | } 82 | } 83 | 84 | /** 85 | * Executes registered after returning advices (after the advised method finishes execution without exceptions). 86 | * 87 | * @param InvocationContext $context invocation context 88 | * @param $result result returned from the advised method after its invocation 89 | */ 90 | public function afterReturning(InvocationContext $context, $result) { 91 | foreach ($this->detectInterceptorsForContext($context) as $interceptor) { 92 | if ($interceptor instanceof AfterReturningInterceptor) { 93 | $interceptor->afterReturning($context, $result); 94 | } 95 | } 96 | } 97 | 98 | /** 99 | * Executes registered after advices (after the advised method finishes execution). 100 | * 101 | * @param InvocationContext $context invocation context 102 | * @param $result result returned from the advised method after its invocation 103 | * @return mixed method result that is returned after it has been processed by registered interceptors 104 | */ 105 | public function after(InvocationContext $context, $result) { 106 | foreach ($this->detectInterceptorsForContext($context) as $interceptor) { 107 | if ($interceptor instanceof AfterInvocationInterceptor) { 108 | $result = $interceptor->afterInvocation($context, $result); 109 | } 110 | } 111 | return $result; 112 | } 113 | 114 | private function detectInterceptorsForContext(InvocationContext $context) { 115 | $methodName = $context->getMethodName(); 116 | if ($methodName === null) { 117 | $methodName = '*'; 118 | } 119 | 120 | $className = $context->getClassName(); 121 | if (!preg_match('/^\\\\/', $className)) { 122 | $className = '\\'.$className; 123 | } 124 | 125 | $classNameAnnotations = $this->annotations[$className]; 126 | 127 | if (!isset($classNameAnnotations)) { 128 | return array(); 129 | } 130 | 131 | if (!array_key_exists($methodName, $classNameAnnotations)) { 132 | $classNameAnnotations[$methodName] = array(); 133 | } 134 | 135 | if (!array_key_exists('*', $classNameAnnotations)) { 136 | $classNameAnnotations['*'] = array(); 137 | } 138 | 139 | $detectedInterceptors = array(); 140 | if ($context->getMethodName() == '__construct') { 141 | $detectedInterceptors = $classNameAnnotations[$methodName]; 142 | } else { 143 | $detectedInterceptors = array_merge($classNameAnnotations['*'], $classNameAnnotations[$methodName]); 144 | } 145 | 146 | /** select interceptors which accept the name of the called method */ 147 | $result = array(); 148 | array_walk($detectedInterceptors, function ($interceptor) use ($context, &$result) { 149 | if (!($interceptor instanceof SimpleAnnotation) && $interceptor->acceptMethod($context->getMethodName())) { 150 | $interceptor->checkConstraints($context->getTarget()); 151 | $result[] = $interceptor; 152 | } 153 | }); 154 | return $result; 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /lib/Exar/Annotation/Annotation.php: -------------------------------------------------------------------------------- 1 | _name = $rClass->getShortName(); 25 | 26 | $this->_targetMeta = self::extractTargetMeta($target); 27 | 28 | $rClass = new \ReflectionClass($this); 29 | 30 | if (isset($data['value']) && !$this->isAllowValueProperty()) { 31 | trigger_error('Property "value" ist not allowed on annotation "'.$rClass->getName().'"', E_USER_ERROR); 32 | } 33 | 34 | foreach($data as $key => $value) { 35 | if ($key == 'value' || ($rClass->hasProperty($key) && $rClass->getProperty($key)->getDeclaringClass()->getName() != __CLASS__)) { 36 | if ($rClass->getProperty($key)->isPrivate()) { 37 | trigger_error('Property "'.$key.'" within annotation "'.$rClass->getName().'" is private and cannot be set', E_USER_ERROR); 38 | } 39 | $this->$key = $value; 40 | } else { 41 | trigger_error('Property "'.$key.'" ist not allowed on annotation "'.$rClass->getName().'"', E_USER_ERROR); 42 | } 43 | 44 | } 45 | $this->checkTargetAnnotation($target); 46 | $this->checkCreationConstraints(); 47 | } 48 | 49 | /** 50 | * Returns true if this is a single-value annotation (anonymous attribute is activated), else false.
51 | * This method can be overridden to define multi-value annotation classes. 52 | * 53 | * @return bool if this is a single-value annotation, else false 54 | */ 55 | public function isAllowValueProperty() { 56 | return $this->_allowValueProperty; 57 | } 58 | 59 | /** 60 | * Returns the target class. 61 | * 62 | * @return string target class 63 | */ 64 | public function getTargetClass() { 65 | return $this->_targetMeta['class']; 66 | } 67 | 68 | /** 69 | * Return the target method 70 | * 71 | * @return string target method 72 | */ 73 | public function getTargetMethod() { 74 | return $this->_targetMeta['method']; 75 | } 76 | 77 | /** 78 | * Returns the annotation name. 79 | * 80 | * @return string annotation name 81 | */ 82 | public function getName() { 83 | return $this->_name; 84 | } 85 | 86 | /** 87 | * Checks the annotation constraints.
88 | * This method can be overridden to define specific conditions (e.g. certain method modifiers). 89 | * 90 | * @param $targetObject annotation's target 91 | */ 92 | public function checkConstraints($targetObject) {} 93 | 94 | /** 95 | * This method can be used at runtime to decide if the target method of the annotation has to be intercepted.
96 | * This will be needed for class annotations that propagates specific behavior for one or more methods. 97 | * 98 | * @param $methodName method name 99 | * @return bool true if the specified method is applied to this annotation, else false 100 | */ 101 | public function acceptMethod($methodName) { 102 | return true; 103 | } 104 | 105 | protected function setAllowValueProperty($allowValueProperty) { 106 | $this->_allowValueProperty = $allowValueProperty; 107 | } 108 | 109 | protected function setTargetClass($class) { 110 | $this->_targetMeta['class'] = $class; 111 | } 112 | 113 | protected function setTargetMethod($method) { 114 | $this->_targetMeta['method'] = $method; 115 | } 116 | 117 | protected function checkCreationConstraints() {} 118 | 119 | protected static function checkBooleanConstraint($val, $errorMsg = null) { 120 | if (!$val) { 121 | self::triggerError($errorMsg); 122 | } 123 | } 124 | 125 | protected static function triggerError($errorMsg = null) { 126 | trigger_error($errorMsg === null ? 'Annotation constraint violation' : $errorMsg, E_USER_ERROR); 127 | } 128 | 129 | private function checkTargetAnnotation($target) { 130 | $rClass = new ReflectionClass(get_class($this)); 131 | if ($rClass->hasAnnotation('Target')) { // @Target is set 132 | $value = $rClass->getAnnotation('Target')->value; // read the value attribute of @Target 133 | $values = is_array($value) ? $value : array($value); // convert string value into an array if needed 134 | foreach($values as $value) { 135 | $value = strtolower($value); 136 | if ($value == 'class' && $target instanceof \ReflectionClass) return; 137 | if ($value == 'method' && $target instanceof \ReflectionMethod) return; 138 | if ($value == 'property' && $target instanceof \ReflectionProperty) return; 139 | } 140 | trigger_error('Annotation "'.get_class($this).'" is not allowed on "'.$target->getName().'"', E_USER_ERROR); 141 | } 142 | } 143 | 144 | private static function extractTargetMeta($target) { 145 | $meta = array(); 146 | 147 | if ($target instanceof \ReflectionClass) { 148 | $meta['class'] = '\\'.$target->getName(); 149 | $meta['method'] = '*'; 150 | } elseif ($target instanceof \ReflectionMethod) { 151 | $meta['class'] = '\\'.$target->getDeclaringClass()->getName(); 152 | $meta['method'] = $target->getName(); 153 | } elseif ($target instanceof \ReflectionProperty) { 154 | $meta['class'] = '\\'.$target->getDeclaringClass()->getName(); 155 | $meta['method'] = '__construct'; 156 | } else { 157 | trigger_error('Illegal target type ('.get_class($target).')', E_USER_ERROR); 158 | } 159 | 160 | return $meta; 161 | } 162 | } -------------------------------------------------------------------------------- /lib/Exar/RestHandler.php: -------------------------------------------------------------------------------- 1 | 'Continue', 79 | 101 => 'Switching Protocols', 80 | 102 => 'Processing', 81 | 82 | 200 => 'OK', 83 | 201 => 'Created', 84 | 202 => 'Accepted', 85 | 203 => 'Non-Authoritative Information', 86 | 204 => 'No Content', 87 | 205 => 'Reset Content', 88 | 206 => 'Partial Content', 89 | 207 => 'Multi-Status', 90 | 226 => 'IM Used', 91 | 92 | 300 => 'Multiple Choices', 93 | 301 => 'Moved Permanently', 94 | 302 => 'Found', 95 | 303 => 'See Other', 96 | 304 => 'Not Modified', 97 | 305 => 'Use Proxy', 98 | 306 => 'Reserved', 99 | 307 => 'Temporary Redirect', 100 | 101 | 400 => 'Bad Request', 102 | 401 => 'Unauthorized', 103 | 402 => 'Payment Required', 104 | 403 => 'Forbidden', 105 | 404 => 'Not Found', 106 | 405 => 'Method Not Allowed', 107 | 406 => 'Not Acceptable', 108 | 407 => 'Proxy Authentication Required', 109 | 408 => 'Request Timeout', 110 | 409 => 'Conflict', 111 | 410 => 'Gone', 112 | 411 => 'Length Required', 113 | 412 => 'Precondition Failed', 114 | 413 => 'Request Entity Too Large', 115 | 414 => 'Request-URI Too Long', 116 | 415 => 'Unsupported Media Type', 117 | 416 => 'Requested Range Not Satisfiable', 118 | 417 => 'Expectation Failed', 119 | 422 => 'Unprocessable Entity', 120 | 423 => 'Locked', 121 | 424 => 'Failed Dependency', 122 | 426 => 'Upgrade Required', 123 | 124 | 500 => 'Internal Server Error', 125 | 501 => 'Not Implemented', 126 | 502 => 'Bad Gateway', 127 | 503 => 'Service Unavailable', 128 | 504 => 'Gateway Timeout', 129 | 505 => 'HTTP Version Not Supported', 130 | 506 => 'Variant Also Negotiates', 131 | 507 => 'Insufficient Storage', 132 | 510 => 'Not Extended' 133 | ); 134 | 135 | static private $routeCollector = null; 136 | static private $restDispatcher = null; 137 | 138 | static public function dispatch(array $classes) { 139 | self::$routeCollector = new \FastRoute\RouteCollector( 140 | new \FastRoute\RouteParser\Std(), 141 | new \FastRoute\DataGenerator\GroupCountBased() 142 | ); 143 | 144 | self::activate($classes); 145 | 146 | self::$restDispatcher = new \FastRoute\Dispatcher\GroupCountBased(self::$routeCollector->getData()); 147 | 148 | $httpMethod = $_SERVER['REQUEST_METHOD']; 149 | $uri = $_SERVER['REQUEST_URI']; 150 | 151 | $routeInfo = self::$restDispatcher->dispatch($httpMethod, $uri); 152 | switch ($routeInfo[0]) { 153 | case \FastRoute\Dispatcher::NOT_FOUND: 154 | self::sendResponseCode(self::HTTP_NOT_FOUND); 155 | break; 156 | case \FastRoute\Dispatcher::METHOD_NOT_ALLOWED: 157 | $allowedMethods = $routeInfo[1]; 158 | self::sendResponseCode(self::HTTP_METHOD_NOT_ALLOWED); 159 | break; 160 | case \FastRoute\Dispatcher::FOUND: 161 | self::sendResponseCode(self::HTTP_OK); 162 | $handler = $routeInfo[1]; 163 | $vars = $routeInfo[2]; 164 | 165 | $handlerParts = explode('::', $handler); 166 | $obj = new $handlerParts[0]; 167 | echo call_user_func_array(array($obj, $handlerParts[1]), $vars); 168 | break; 169 | } 170 | 171 | } 172 | 173 | static public function sendResponseCode($num) { 174 | $protocol = empty($_SERVER['SERVER_PROTOCOL']) ? 'HTTP/1.1' : $_SERVER['SERVER_PROTOCOL']; 175 | 176 | $str = array_key_exists($num, self::$status) ? self::$status[$num] : ''; 177 | $header = "$protocol $num $str"; 178 | 179 | header($header, true, false); 180 | } 181 | 182 | static private function activate(array $classes) { 183 | foreach ($classes as $cl) { 184 | $rClass = new ReflectionClass($cl); 185 | 186 | foreach ($rClass->getMethods(\ReflectionMethod::IS_PUBLIC) as $rMethod) { 187 | self::processMethod($rMethod); 188 | } 189 | } 190 | } 191 | 192 | static private function processMethod(ReflectionMethod $method) { 193 | if (!$method->hasAnnotation('Path')) { // method must be annotated with @Path to be processed 194 | return; 195 | } 196 | 197 | $path = $method->getAnnotation('Path'); 198 | self::$routeCollector->addRoute(self::getHttpMethod($method), $path->getValue(), $method->getDeclaringClass()->getName().'::'.$method->getName()); 199 | } 200 | 201 | static private function getHttpMethod(ReflectionMethod $method) { 202 | foreach (array('GET', 'PUT', 'POST', 'DELETE') as $ann) { 203 | if ($method->hasAnnotation($ann)) { 204 | return $ann; 205 | } 206 | } 207 | return 'GET'; 208 | } 209 | 210 | } -------------------------------------------------------------------------------- /lib/Exar/Aop/Weaver.php: -------------------------------------------------------------------------------- 1 | parser = new \PhpParser\Parser(new \PhpParser\Lexer); 18 | $this->prettyPrinter = new \PhpParser\PrettyPrinter\Standard; 19 | 20 | /* check cache directory */ 21 | if (!file_exists($cacheDir)) { 22 | mkdir($cacheDir, 0777); 23 | } 24 | 25 | $this->cacheDir = realpath($cacheDir); 26 | 27 | if (!is_dir($this->cacheDir)) { 28 | throw new \InvalidArgumentException('Cache directory is not a directory: ' . $this->cacheDir); 29 | } 30 | 31 | if (!is_writable($this->cacheDir)) { 32 | throw new \InvalidArgumentException('Cache directory is not writable: ' . $this->cacheDir); 33 | } 34 | 35 | } 36 | 37 | public function getCacheDir() { 38 | return $this->cacheDir; 39 | } 40 | 41 | public function process($file) { 42 | if (!file_exists($file)) { 43 | throw new \InvalidArgumentException('File does not exist: ' . $file); 44 | } 45 | 46 | $code = file_get_contents($file); 47 | 48 | try { 49 | $stmts = $this->parser->parse($code); 50 | 51 | $classes = array(); 52 | foreach ($stmts as $stmt) { 53 | $classes = $this->processStmt($stmt); 54 | } 55 | 56 | if (count($classes) == 0) { // no properly annotated classes found, so just include the file 57 | require_once $file; 58 | return true; 59 | } 60 | 61 | $newCode = 'prettyPrinter->prettyPrint($stmts) . PHP_EOL . PHP_EOL; 62 | 63 | $pathInfo = pathinfo($file); 64 | $cachedFileName = $this->cacheDir . DIRECTORY_SEPARATOR . $pathInfo['filename'] . '___' . md5($file) . '.' . $pathInfo['extension']; 65 | 66 | foreach($classes as $cl) { 67 | $newCode .= self::NAMESPACE_SEPARATOR . AnnotationProcessor::CLASSNAME . '::processAnnotations("\\' . $cl . '");' . PHP_EOL; 68 | } 69 | 70 | file_put_contents($cachedFileName, $newCode); 71 | 72 | require_once $cachedFileName; 73 | return true; 74 | 75 | } catch (PhpParser\Error $e) { 76 | // TODO handle parse error 77 | } 78 | } 79 | 80 | private function processStmt(\PhpParser\Node\Stmt $stmt, $namespace = null) { 81 | $classes = array(); 82 | 83 | if ($stmt instanceof \PhpParser\Node\Stmt\Class_) { // class definition found 84 | $className = $this->processClass($stmt, $namespace); 85 | if ($className) { 86 | $classes[] = $namespace . self::NAMESPACE_SEPARATOR . $className; 87 | } 88 | } else { 89 | if ($stmt instanceof \PhpParser\Node\Stmt\Namespace_) { 90 | $namespace = implode(self::NAMESPACE_SEPARATOR, $stmt->name->parts); 91 | } 92 | $subNodes = $stmt->getSubNodeNames(); 93 | if ($stmt->stmts !== null && in_array('stmts', $subNodes)) { 94 | foreach ($stmt->stmts as $s) { 95 | if ($s instanceof \PhpParser\Node\Stmt) { 96 | $classes = array_merge($classes, $this->processStmt($s, $namespace)); 97 | } 98 | } 99 | } 100 | } 101 | 102 | return $classes; 103 | } 104 | 105 | private function processClass(\PhpParser\Node\Stmt\Class_ $class, $namespace) { 106 | if (!strpos($class->getDocComment(), '@Exar')) { // no @Exar annotation found 107 | return false; 108 | } 109 | 110 | $wrapperMethods = array(); 111 | $constructor = null; 112 | foreach ($class->stmts as $stmt) { 113 | if ($stmt instanceof \PhpParser\Node\Stmt\ClassMethod) { 114 | if ($stmt->isStatic()) { // do not consider static methods 115 | continue; 116 | } 117 | 118 | $wrapperStmts = $this->processClassMethod($stmt); 119 | if ($wrapperStmts !== null) { 120 | $wrapperMethods[] = $wrapperStmts; 121 | 122 | if($wrapperStmts->name == '__construct') { 123 | $constructor = $stmt; 124 | } 125 | } 126 | } 127 | } 128 | 129 | if ($constructor === null) { // there are no constructors defined within the current class 130 | $wrapperMethods = array_merge($wrapperMethods, $this->handleConstructors($class, $namespace, $constructor)); 131 | } 132 | 133 | $class->stmts = array_merge($class->stmts, $wrapperMethods); 134 | return $class->name; 135 | } 136 | 137 | private function getClassWithConstructor($class) { 138 | if ($class->hasMethod('__construct')) { // constructor found 139 | return $class; // return reflection class which defines the constructor 140 | } 141 | 142 | if ($class->getParentClass()) { // parent class is available 143 | return $this->getConstructorParams($class->getParentClass()); // look in the parent class 144 | } 145 | 146 | return null; // no constructor found 147 | } 148 | 149 | private function processClassMethod(\PhpParser\Node\Stmt\ClassMethod $method, $callParentClass = false) { 150 | $newMethod = $this->generateMethodForClassMethod($method, $callParentClass); 151 | return $newMethod; 152 | } 153 | 154 | private function generateMethodForClassMethod(\PhpParser\Node\Stmt\ClassMethod $method, $callParentClass = true) { 155 | $modifiers = array(); 156 | 157 | if ($method->isFinal()) { $modifiers[] = 'final'; } 158 | if ($method->isAbstract()) { $modifiers[] = 'abstract'; } 159 | if ($method->isStatic()) { $modifiers[] = 'static'; } 160 | if ($method->isPublic()) { $modifiers[] = 'public'; } 161 | if ($method->isProtected()) { $modifiers[] = 'protected'; } 162 | if ($method->isPrivate()) { $modifiers[] = 'private'; } 163 | 164 | $newMethod = clone $method; 165 | $newMethodBody = $this->generateWrapperMethodBody($method, $callParentClass); 166 | $newMethod->stmts = $this->parser->parse('type = $method->type & 56 | 4; // remove public/protected modifiers and make the method private 170 | $method->name .= self::METHOD_NAME_SUFFIX; // the original method gets suffix 171 | $method->setAttribute('comments', array()); // remove all comments from the original method 172 | 173 | return $newMethod; 174 | } 175 | 176 | private function generateWrapperMethodBody(\PhpParser\Node\Stmt\ClassMethod $method, $callParentClass = false) { 177 | $paramNames = array(); 178 | foreach($method->params as $param) { 179 | $paramNames[] = $param->name; 180 | } 181 | return $this->generateMethodBody($method->name, $paramNames, $callParentClass); 182 | } 183 | 184 | private function generateMethodBody($methodName, $paramNames, $callParentClass = false) { 185 | $params = array(); 186 | foreach ($paramNames as $p) { 187 | $params[] = "'".$p."'".'=>'.'$'.$p; 188 | } 189 | 190 | /* 191 | * name of the context, interceptor manager and the result var should be changed, otherwise there can be a name 192 | * collision if a parameter of the invoked wrapper method has the same name 193 | */ 194 | $hash = uniqid(); 195 | $ctxVar = '$ctx'.$hash; 196 | $imVar = '$im'.$hash; 197 | $resultVar = '$result'.$hash; 198 | 199 | if ($methodName == '__construct' && $callParentClass) { 200 | $methodCall = "'parent::__construct'"; 201 | } else { 202 | $methodCall = "array(\$this, '".$methodName.self::METHOD_NAME_SUFFIX."')"; 203 | } 204 | 205 | $code = "{$ctxVar} = new \\Exar\\Aop\\InvocationContext(\$this, __CLASS__, '{$methodName}', array(".implode(', ', $params).")); 206 | {$imVar} = \\Exar\\Aop\\InterceptorManager::getInstance(); 207 | try { 208 | {$imVar}->before({$ctxVar}); 209 | {$resultVar} = call_user_func_array({$methodCall}, array_values({$ctxVar}->getParams())); 210 | {$imVar}->afterReturning({$ctxVar}, {$resultVar}); 211 | } catch (\\Exar\\Annotation\\Interceptor\\InterceptorException \$e) { 212 | if (\$e->getObject() === \$this) throw \$e; 213 | {$resultVar} = \$e->getResult(); 214 | } catch (\\Exception \$e) { 215 | {$ctxVar}->setException(\$e); 216 | {$imVar}->afterThrowing({$ctxVar}); 217 | if (!isset({$resultVar})) { {$resultVar} = null; } 218 | } 219 | {$resultVar} = {$imVar}->after({$ctxVar}, {$resultVar}); 220 | if ({$ctxVar}->hasException()) { throw {$ctxVar}->getException(); } 221 | return {$resultVar};".PHP_EOL; 222 | 223 | return $code; 224 | } 225 | 226 | /** 227 | * @param \PhpParser\Node\Stmt\Class_ $class 228 | * @param $namespace 229 | * @param $constructor 230 | * @return array 231 | */ 232 | private function handleConstructors(\PhpParser\Node\Stmt\Class_ $class, $namespace, $constructor) { 233 | $wrapperMethods = array(); 234 | 235 | $parentClassWithConstructor = null; // initially, assume that there is no parent class which defines constructor 236 | 237 | if ($class->extends !== null) { // look for constructor definition in parent classes 238 | $parentClass = new \ReflectionClass($namespace . self::NAMESPACE_SEPARATOR . $class->extends->getFirst()); 239 | $parentClassWithConstructor = $this->getClassWithConstructor($parentClass); 240 | } 241 | 242 | $factory = new \PhpParser\BuilderFactory; 243 | $constructorNode = $factory->method('__construct')->makePublic(); 244 | 245 | if ($parentClassWithConstructor !== null) { // add parameters to constructor (if any found) 246 | foreach ($parentClassWithConstructor->getMethod('__construct')->getParameters() as $p) { 247 | $param = $factory->param($p->getName()); 248 | if ($p->isOptional()) { 249 | $param->setDefault($p->getDefaultValue()); 250 | } 251 | $constructorNode->addParam($param); 252 | } 253 | } 254 | 255 | $constructorNode = $constructorNode->getNode(); 256 | 257 | if ($parentClassWithConstructor === null) { // add an empty constructor if there are no constructors available at all 258 | $wrapperMethods[] = $constructorNode; 259 | } 260 | 261 | $wrapperMethods[] = $this->processClassMethod($constructorNode, $parentClassWithConstructor !== null); 262 | 263 | return $wrapperMethods; 264 | } 265 | 266 | } --------------------------------------------------------------------------------