From f835108534568330d860af9b2de58cd44bb82bad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20Proch=C3=A1zka?= Date: Mon, 26 Aug 2013 22:22:26 +0200 Subject: [PATCH] Basic pointcut matching of method, class, within --- .../Matcher/ClassAnnotateWithMatcher.php | 29 ++++ .../Aop/Pointcut/Matcher/ClassMatcher.php | 43 ++++++ .../Aop/Pointcut/Matcher/EvaluateMatcher.php | 29 ++++ .../Aop/Pointcut/Matcher/FilterMatcher.php | 29 ++++ .../Matcher/MethodAnnotateWithMatcher.php | 29 ++++ .../Aop/Pointcut/Matcher/MethodMatcher.php | 46 +++++++ .../Aop/Pointcut/Matcher/SettingMatcher.php | 29 ++++ .../Aop/Pointcut/Matcher/WithinMatcher.php | 43 ++++++ src/Kdyby/Aop/Pointcut/MatcherFactory.php | 121 +++++++++++++++++ src/Kdyby/Aop/Pointcut/Method.php | 85 ++++++++++++ src/Kdyby/Aop/Pointcut/Rule.php | 26 ++++ src/Kdyby/Aop/Pointcut/Rules.php | 85 ++++++++++++ src/Kdyby/Aop/Pointcut/ServiceDefinition.php | 128 ++++++++++++++++++ tests/KdybyTests/Aop/PointcutRules.phpt | 94 +++++++++++++ .../Aop/files/pointcut-examples.php | 80 +++++++++++ tests/tmp/.gitignore | 2 + 16 files changed, 898 insertions(+) create mode 100644 src/Kdyby/Aop/Pointcut/Matcher/ClassAnnotateWithMatcher.php create mode 100644 src/Kdyby/Aop/Pointcut/Matcher/ClassMatcher.php create mode 100644 src/Kdyby/Aop/Pointcut/Matcher/EvaluateMatcher.php create mode 100644 src/Kdyby/Aop/Pointcut/Matcher/FilterMatcher.php create mode 100644 src/Kdyby/Aop/Pointcut/Matcher/MethodAnnotateWithMatcher.php create mode 100644 src/Kdyby/Aop/Pointcut/Matcher/MethodMatcher.php create mode 100644 src/Kdyby/Aop/Pointcut/Matcher/SettingMatcher.php create mode 100644 src/Kdyby/Aop/Pointcut/Matcher/WithinMatcher.php create mode 100644 src/Kdyby/Aop/Pointcut/MatcherFactory.php create mode 100644 src/Kdyby/Aop/Pointcut/Method.php create mode 100644 src/Kdyby/Aop/Pointcut/Rule.php create mode 100644 src/Kdyby/Aop/Pointcut/Rules.php create mode 100644 src/Kdyby/Aop/Pointcut/ServiceDefinition.php create mode 100644 tests/KdybyTests/Aop/PointcutRules.phpt create mode 100644 tests/KdybyTests/Aop/files/pointcut-examples.php create mode 100755 tests/tmp/.gitignore diff --git a/src/Kdyby/Aop/Pointcut/Matcher/ClassAnnotateWithMatcher.php b/src/Kdyby/Aop/Pointcut/Matcher/ClassAnnotateWithMatcher.php new file mode 100644 index 0000000..5d3952e --- /dev/null +++ b/src/Kdyby/Aop/Pointcut/Matcher/ClassAnnotateWithMatcher.php @@ -0,0 +1,29 @@ + + */ +class ClassAnnotateWithMatcher extends Nette\Object implements Kdyby\Aop\Pointcut\Rule +{ + + public function matches(Kdyby\Aop\Pointcut\Method $method) + { + + } + +} diff --git a/src/Kdyby/Aop/Pointcut/Matcher/ClassMatcher.php b/src/Kdyby/Aop/Pointcut/Matcher/ClassMatcher.php new file mode 100644 index 0000000..2926604 --- /dev/null +++ b/src/Kdyby/Aop/Pointcut/Matcher/ClassMatcher.php @@ -0,0 +1,43 @@ + + */ +class ClassMatcher extends Nette\Object implements Kdyby\Aop\Pointcut\Rule +{ + + /** + * @var string + */ + private $class; + + + + public function __construct($class) + { + $this->class = str_replace('\\*', '.*', preg_quote($class)); + } + + + + public function matches(Kdyby\Aop\Pointcut\Method $method) + { + return preg_match('~^' . $this->class . '\z~i', $method->getClassName()); + } + +} diff --git a/src/Kdyby/Aop/Pointcut/Matcher/EvaluateMatcher.php b/src/Kdyby/Aop/Pointcut/Matcher/EvaluateMatcher.php new file mode 100644 index 0000000..f372b61 --- /dev/null +++ b/src/Kdyby/Aop/Pointcut/Matcher/EvaluateMatcher.php @@ -0,0 +1,29 @@ + + */ +class EvaluateMatcher extends Nette\Object implements Kdyby\Aop\Pointcut\Rule +{ + + public function matches(Kdyby\Aop\Pointcut\Method $method) + { + + } + +} diff --git a/src/Kdyby/Aop/Pointcut/Matcher/FilterMatcher.php b/src/Kdyby/Aop/Pointcut/Matcher/FilterMatcher.php new file mode 100644 index 0000000..0aa77b7 --- /dev/null +++ b/src/Kdyby/Aop/Pointcut/Matcher/FilterMatcher.php @@ -0,0 +1,29 @@ + + */ +class FilterMatcher extends Nette\Object implements Kdyby\Aop\Pointcut\Rule +{ + + public function matches(Kdyby\Aop\Pointcut\Method $method) + { + + } + +} diff --git a/src/Kdyby/Aop/Pointcut/Matcher/MethodAnnotateWithMatcher.php b/src/Kdyby/Aop/Pointcut/Matcher/MethodAnnotateWithMatcher.php new file mode 100644 index 0000000..11c4391 --- /dev/null +++ b/src/Kdyby/Aop/Pointcut/Matcher/MethodAnnotateWithMatcher.php @@ -0,0 +1,29 @@ + + */ +class MethodAnnotateWithMatcher extends Nette\Object implements Kdyby\Aop\Pointcut\Rule +{ + + public function matches(Kdyby\Aop\Pointcut\Method $method) + { + + } + +} diff --git a/src/Kdyby/Aop/Pointcut/Matcher/MethodMatcher.php b/src/Kdyby/Aop/Pointcut/Matcher/MethodMatcher.php new file mode 100644 index 0000000..0334ec4 --- /dev/null +++ b/src/Kdyby/Aop/Pointcut/Matcher/MethodMatcher.php @@ -0,0 +1,46 @@ + + */ +class MethodMatcher extends Nette\Object implements Kdyby\Aop\Pointcut\Rule +{ + + /** + * @var string + */ + private $method; + + + + /** + * @todo visibility + */ + public function __construct($method) + { + $this->method = str_replace('\\*', '.*', preg_quote($method)); + } + + + + public function matches(Kdyby\Aop\Pointcut\Method $method) + { + return preg_match('~^' . $this->method . '\z~i', $method->getName()); + } + +} diff --git a/src/Kdyby/Aop/Pointcut/Matcher/SettingMatcher.php b/src/Kdyby/Aop/Pointcut/Matcher/SettingMatcher.php new file mode 100644 index 0000000..4e8e65a --- /dev/null +++ b/src/Kdyby/Aop/Pointcut/Matcher/SettingMatcher.php @@ -0,0 +1,29 @@ + + */ +class SettingMatcher extends Nette\Object implements Kdyby\Aop\Pointcut\Rule +{ + + public function matches(Kdyby\Aop\Pointcut\Method $method) + { + + } + +} diff --git a/src/Kdyby/Aop/Pointcut/Matcher/WithinMatcher.php b/src/Kdyby/Aop/Pointcut/Matcher/WithinMatcher.php new file mode 100644 index 0000000..aac60d7 --- /dev/null +++ b/src/Kdyby/Aop/Pointcut/Matcher/WithinMatcher.php @@ -0,0 +1,43 @@ + + */ +class WithinMatcher extends Nette\Object implements Kdyby\Aop\Pointcut\Rule +{ + + /** + * @var string + */ + private $type; + + + + public function __construct($type) + { + $this->type = Nette\Reflection\ClassType::from($type)->getName(); + } + + + + public function matches(Kdyby\Aop\Pointcut\Method $method) + { + return isset($method->typesWithin[$this->type]); + } + +} diff --git a/src/Kdyby/Aop/Pointcut/MatcherFactory.php b/src/Kdyby/Aop/Pointcut/MatcherFactory.php new file mode 100644 index 0000000..b19e273 --- /dev/null +++ b/src/Kdyby/Aop/Pointcut/MatcherFactory.php @@ -0,0 +1,121 @@ + + */ +class MatcherFactory extends Nette\Object +{ + + /** + * @var \Nette\DI\ContainerBuilder + */ + private $builder; + + /** + * @var \Doctrine\Common\Annotations\Reader + */ + private $annotationReader; + + /** + * @var array + */ + private $cache = array(); + + + + public function __construct(Nette\DI\ContainerBuilder $builder, Reader $annotationReader) + { + $this->builder = $builder; + $this->annotationReader = $annotationReader; + } + + + + /** + * @param string $type + * @param string $arg + * @return Rule + */ + public function getMatcher($type, $arg) + { + if (!isset($this->cache[$type][$arg])) { + $this->cache[$type][$arg] = call_user_func(array($this, 'create' . ucfirst($type)), $arg); + } + + return $this->cache[$type][$arg]; + } + + + + public function createClass($class) + { + return new Matcher\ClassMatcher($class); + } + + + + public function createMethod($method) + { + return new Matcher\MethodMatcher($method); + } + + + + public function createWithin($within) + { + return new Matcher\WithinMatcher($within); + } + + + + public function createFilter($filterClass) + { + return new Matcher\FilterMatcher($filterClass); + } + + + + public function createSetting($setting) + { + return new Matcher\SettingMatcher($setting, $this->builder); + } + + + + public function createEvaluate($evaluate) + { + return new Matcher\EvaluateMatcher($evaluate, $this->builder); + } + + + + public function createClassAnnotatedWith($annotation) + { + return new Matcher\ClassAnnotateWithMatcher($annotation, $this->annotationReader); + } + + + + public function createMethodAnnotatedWith($annotation) + { + return new Matcher\MethodAnnotateWithMatcher($annotation, $this->annotationReader); + } + +} diff --git a/src/Kdyby/Aop/Pointcut/Method.php b/src/Kdyby/Aop/Pointcut/Method.php new file mode 100644 index 0000000..d053dd8 --- /dev/null +++ b/src/Kdyby/Aop/Pointcut/Method.php @@ -0,0 +1,85 @@ + + * + * @property array|string[] $typesWithin + * @property-read array|string[] $typesWithin + */ +class Method extends Nette\Object +{ + + /** + * @var \Nette\Reflection\Method + */ + private $method; + + /** + * @var ServiceDefinition + */ + private $serviceDefinition; + + + + public function __construct(Nette\Reflection\Method $method, ServiceDefinition $serviceDefinition) + { + $this->method = $method; + $this->serviceDefinition = $serviceDefinition; + } + + + + /** + * @return string + */ + public function getName() + { + return $this->method->getName(); + } + + + + /** + * @return string + */ + public function getClassName() + { + return $this->serviceDefinition->getTypeReflection()->getName(); + } + + + + /** + * @return array + */ + public function getTypesWithin() + { + return $this->serviceDefinition->getTypesWithin(); + } + + + + /** + * @return Nette\Reflection\Method + */ + public function unwrap() + { + return $this->method; + } + +} diff --git a/src/Kdyby/Aop/Pointcut/Rule.php b/src/Kdyby/Aop/Pointcut/Rule.php new file mode 100644 index 0000000..8bf09db --- /dev/null +++ b/src/Kdyby/Aop/Pointcut/Rule.php @@ -0,0 +1,26 @@ + + */ +interface Rule +{ + + function matches(Method $method); + +} diff --git a/src/Kdyby/Aop/Pointcut/Rules.php b/src/Kdyby/Aop/Pointcut/Rules.php new file mode 100644 index 0000000..56819ce --- /dev/null +++ b/src/Kdyby/Aop/Pointcut/Rules.php @@ -0,0 +1,85 @@ + + */ +class Rules extends Nette\Object implements Rule +{ + + const OP_AND = 'AND'; + const OP_OR = 'OR'; + + /** + * @var string + */ + private $operator; + + /** + * @var array|Rule[] + */ + private $rules; + + + + public function __construct(array $rules = array(), $operator = self::OP_AND) + { + foreach ($rules as $rule) { + $this->addRule($rule); + } + + $this->operator = $operator; + } + + + + public function addRule(Rule $rule) + { + $this->rules[] = $rule; + } + + + + /** + * @param Method $method + * @return bool + */ + public function matches(Method $method) + { + $logical = array(); + foreach ($this->rules as $rule) { + $logical[] = $rule->matches($method); + if (!$this->isMatching($logical)) { + return FALSE; + } + } + + return $this->isMatching($logical); + } + + + + private function isMatching(array $result) + { + if ($this->operator === self::OP_AND) { + return array_filter($result) === $result; // all values are TRUE + } + + return (bool) array_filter($result); // at least one is TRUE + } + +} diff --git a/src/Kdyby/Aop/Pointcut/ServiceDefinition.php b/src/Kdyby/Aop/Pointcut/ServiceDefinition.php new file mode 100644 index 0000000..1de8529 --- /dev/null +++ b/src/Kdyby/Aop/Pointcut/ServiceDefinition.php @@ -0,0 +1,128 @@ + + */ +class ServiceDefinition extends Nette\Object +{ + + /** + * @var \Nette\DI\ServiceDefinition + */ + private $serviceDefinition; + + /** + * @var \Nette\Reflection\ClassType + */ + private $originalType; + + /** + * @var array|Nette\Reflection\Method[] + */ + private $openMethods; + + /** + * @var array + */ + private $typesWithing; + + + + public function __construct(Nette\DI\ServiceDefinition $def) + { + $this->serviceDefinition = $def; + + if (empty($def->class)) { + throw new Kdyby\Aop\InvalidArgumentException("Given service definition has unresolved class, please specify service type explicitly."); + } + + $this->originalType = Nette\Reflection\ClassType::from($def->class); + } + + + + /** + * @return Nette\Reflection\ClassType + */ + public function getTypeReflection() + { + return $this->originalType; + } + + + + /** + * @return array + */ + public function getTypesWithin() + { + if ($this->typesWithing !== NULL) { + return $this->typesWithing; + } + + return $this->typesWithing = class_parents($class = $this->originalType->getName()) + class_implements($class) + array($class => $class); + } + + + + /** + * @return array|Method[] + */ + public function getOpenMethods() + { + if ($this->openMethods !== NULL) { + return $this->openMethods; + } + + $this->openMethods = array(); + $type = $this->originalType; + do { + foreach ($type->getMethods(\ReflectionMethod::IS_PUBLIC | \ReflectionMethod::IS_PROTECTED) as $method) { + if ($method->isFinal()) { + continue; // todo: maybe in next version + } + + $this->openMethods[$method->getName()] = new Method($method, $this); + } + + } while ($type = $type->getParentClass()); + + return $this->openMethods; + } + + + + /** + * @param Rule $rule + * @return array|Nette\Reflection\Method[] + */ + public function match(Rule $rule) + { + $matching = array(); + foreach ($this->getOpenMethods() as $method) { + if ($rule->matches($method)) { + $matching[] = $method->unwrap(); + } + } + + return $matching; + } + +} diff --git a/tests/KdybyTests/Aop/PointcutRules.phpt b/tests/KdybyTests/Aop/PointcutRules.phpt new file mode 100644 index 0000000..e46f0f0 --- /dev/null +++ b/tests/KdybyTests/Aop/PointcutRules.phpt @@ -0,0 +1,94 @@ + + * @package Kdyby\Aop + */ + +namespace KdybyTests\Aop; + +use Kdyby; +use Kdyby\Aop\Pointcut; +use Kdyby\Aop\Pointcut\Matcher; +use Nette; +use Tester; +use Tester\Assert; + +require_once __DIR__ . '/../bootstrap.php'; +require_once __DIR__ . '/files/pointcut-examples.php'; + + + +/** + * @author Filip Procházka + */ +class PointcutRulesTest extends Tester\TestCase +{ + + public function dataMatch() + { + $data = array(); + + $data[] = array(TRUE, + new Pointcut\Rules(array(new Matcher\ClassMatcher('KdybyTests\Aop\SmegHead'))), + $this->createDefinition('KdybyTests\Aop\SmegHead'), + ); + + $data[] = array(TRUE, + new Pointcut\Rules(array(new Matcher\ClassMatcher('KdybyTests\Aop\*'))), + $this->createDefinition('KdybyTests\Aop\SmegHead'), + ); + + $data[] = array(TRUE, + new Pointcut\Rules(array(new Matcher\ClassMatcher('*'))), + $this->createDefinition('KdybyTests\Aop\SmegHead'), + ); + + $data[] = array(FALSE, + new Pointcut\Rules(array(new Matcher\ClassMatcher('KdybyTests\Aop\SmegHead'))), + $this->createDefinition('KdybyTests\Aop\Legie'), + ); + + $data[] = array(TRUE, + new Pointcut\Rules(array(new Matcher\WithinMatcher('KdybyTests\Aop\Cat'))), + $this->createDefinition('KdybyTests\Aop\Legie'), + ); + + $data[] = array(FALSE, + new Pointcut\Rules(array(new Matcher\WithinMatcher('KdybyTests\Aop\Cat'))), + $this->createDefinition('KdybyTests\Aop\SmegHead'), + ); + + return $data; + } + + + + /** + * @dataProvider dataMatch + */ + public function testMatch($expected, Kdyby\Aop\Pointcut\Rule $rules, Kdyby\Aop\Pointcut\ServiceDefinition $def) + { + Assert::same($expected, (bool) $def->match($rules)); + } + + + + /** + * @param string $class + * @return Pointcut\ServiceDefinition + */ + private function createDefinition($class) + { + $def = new Nette\DI\ServiceDefinition(); + $def->setClass($class); + + return new Pointcut\ServiceDefinition($def); + } + +} + +\run(new PointcutRulesTest()); diff --git a/tests/KdybyTests/Aop/files/pointcut-examples.php b/tests/KdybyTests/Aop/files/pointcut-examples.php new file mode 100644 index 0000000..dbeb05b --- /dev/null +++ b/tests/KdybyTests/Aop/files/pointcut-examples.php @@ -0,0 +1,80 @@ +