Last active
August 29, 2015 14:20
-
-
Save itsjavi/be11193498473af7a846 to your computer and use it in GitHub Desktop.
PHP Annotation Parser based on Nette AnnotationParser.php
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <?php | |
| /** | |
| * Annotations support for PHP. | |
| * Class based on the Nette Framework annotation parser. | |
| */ | |
| class AnnotationParser | |
| { | |
| /** @internal single & double quoted PHP string */ | |
| const RE_STRING = '\'(?:\\\\.|[^\'\\\\])*\'|"(?:\\\\.|[^"\\\\])*"'; | |
| /** @internal identifier */ | |
| const RE_IDENTIFIER = '[_a-zA-Z\x7F-\xFF][_a-zA-Z0-9\x7F-\xFF-\\\]*'; | |
| /** @var array */ | |
| protected static $inherited = ['description', 'param', 'return']; | |
| /** | |
| * @var array Annotation namespaces | |
| */ | |
| protected $namespaces = []; | |
| /** | |
| * @param array $namespaces Annotation namespaces | |
| */ | |
| function __construct(array $namespaces = []) | |
| { | |
| $this->namespaces = array_merge($namespaces, [__NAMESPACE__]); | |
| } | |
| /** | |
| * Returns annotations. | |
| * | |
| * @param \ReflectionClass|\ReflectionMethod|\ReflectionProperty $r The reflector object | |
| * | |
| * @return BaseAnnotation[] | |
| */ | |
| public function getAnnotations(\Reflector $r) | |
| { | |
| if ($r instanceof \ReflectionClass) { | |
| $type = $r->getName(); | |
| $member = 'class'; | |
| //$file = $r->getFileName(); | |
| } elseif ($r instanceof \ReflectionMethod) { | |
| $type = $r->getDeclaringClass()->getName(); | |
| $member = $r->getName(); | |
| //$file = $r->getFileName(); | |
| } elseif ($r instanceof \ReflectionFunction) { | |
| $type = null; | |
| $member = $r->getName(); | |
| //$file = $r->getFileName(); | |
| } else { | |
| $type = $r->getDeclaringClass()->getName(); | |
| $member = '$' . $r->getName(); | |
| //$file = $r->getDeclaringClass()->getFileName(); | |
| } | |
| $annotations = $this->parseComment($r->getDocComment()); | |
| // inheritdoc | |
| if ($r instanceof \ReflectionMethod && !$r->isPrivate() | |
| && (!$r->isConstructor() || !empty($annotations['inheritdoc'][0])) | |
| ) { | |
| try { | |
| $inherited = $this->getAnnotations(new \ReflectionMethod(get_parent_class($type), $member)); | |
| } catch (\ReflectionException $e) { | |
| try { | |
| $inherited = $this->getAnnotations($r->getPrototype()); | |
| } catch (\ReflectionException $e) { | |
| $inherited = []; | |
| } | |
| } | |
| $annotations += array_intersect_key($inherited, array_flip(self::$inherited)); | |
| } | |
| $annotationsInstances = []; | |
| foreach ($annotations as $annName => $annValues) { | |
| foreach ($annValues as $annValue) { | |
| $annFullClassName = null; | |
| $annBaseClassName = ucfirst($annName) . 'Annotation'; | |
| foreach ($this->namespaces as $ns) { | |
| $ns = rtrim($ns, '\\') . '\\'; | |
| if (class_exists($ns . $annBaseClassName)) { | |
| $annFullClassName = $ns . $annBaseClassName; | |
| $annotationsInstances[] = new $annFullClassName($annValue); | |
| break; | |
| } | |
| } | |
| if (is_null($annFullClassName)) { | |
| $annotationsInstances[] = new BaseAnnotation($annValue); | |
| } | |
| } | |
| } | |
| return $annotationsInstances; | |
| } | |
| /** | |
| * Parses phpDoc comment. | |
| * | |
| * @param string | |
| * | |
| * @return array | |
| */ | |
| protected function parseComment($comment) | |
| { | |
| static $tokens = ['true' => true, 'false' => false, 'null' => null, '' => true]; | |
| $res = []; | |
| $comment = preg_replace('#^\s*\*\s?#ms', '', trim($comment, '/*')); | |
| $parts = preg_split('#^\s*(?=@' . self::RE_IDENTIFIER . ')#m', $comment, 2); | |
| $description = trim($parts[0]); | |
| if ($description !== '') { | |
| $res['description'] = [$description]; | |
| } | |
| $matches = $this->matchAll( | |
| isset($parts[1]) ? $parts[1] : '', | |
| '~ | |
| (?<=\s|^)@(' . self::RE_IDENTIFIER . ')[ \t]* ## annotation | |
| ( | |
| \((?>' . self::RE_STRING . '|[^\'")@]+)+\)| ## (value) | |
| [^(@\r\n][^@\r\n]*|) ## value | |
| ~xi' | |
| ); | |
| foreach ($matches as $match) { | |
| list(, $name, $value) = $match; | |
| if (substr($value, 0, 1) === '(') { | |
| $items = []; | |
| $key = ''; | |
| $val = true; | |
| $value[0] = ','; | |
| while ($m = $this->match( | |
| $value, | |
| '#\s*,\s*(?>(' . self::RE_IDENTIFIER . ')\s*=\s*)?(' . self::RE_STRING . '|[^\'"),\s][^\'"),]*)#A') | |
| ) { | |
| $value = substr($value, strlen($m[0])); | |
| list(, $key, $val) = $m; | |
| $val = rtrim($val); | |
| if ($val[0] === "'" || $val[0] === '"') { | |
| $val = substr($val, 1, -1); | |
| } elseif (is_numeric($val)) { | |
| $val = 1 * $val; | |
| } else { | |
| $lval = strtolower($val); | |
| $val = array_key_exists($lval, $tokens) ? $tokens[$lval] : $val; | |
| } | |
| if ($key === '') { | |
| $items[] = $val; | |
| } else { | |
| $items[$key] = $val; | |
| } | |
| } | |
| $value = count($items) < 2 && $key === '' ? $val : $items; | |
| } else { | |
| $value = trim($value); | |
| if (is_numeric($value)) { | |
| $value = 1 * $value; | |
| } else { | |
| $lval = strtolower($value); | |
| $value = array_key_exists($lval, $tokens) ? $tokens[$lval] : $value; | |
| } | |
| } | |
| $res[$name][] = $value; | |
| } | |
| return $res; | |
| } | |
| protected function match($subject, $pattern, $flags = 0, $offset = 0) | |
| { | |
| $matches = null; | |
| if ($offset > strlen($subject)) { | |
| return null; | |
| } | |
| return preg_match($pattern, $subject, $matches, $flags, $offset) ? $matches : null; | |
| } | |
| protected function matchAll($subject, $pattern, $flags = 0, $offset = 0) | |
| { | |
| $matches = null; | |
| if ($offset > strlen($subject)) { | |
| return []; | |
| } | |
| preg_match_all( | |
| $pattern, $subject, $matches, | |
| ($flags & PREG_PATTERN_ORDER) ? $flags : ($flags | PREG_SET_ORDER), | |
| $offset | |
| ); | |
| return $matches; | |
| } | |
| } | |
| class BaseAnnotation | |
| { | |
| /** | |
| * Argument separator string | |
| */ | |
| const ARGUMENT_SEPARATOR = ' '; | |
| /** | |
| * Total number of arguments, excluding the comment (that should be at the end) | |
| */ | |
| const ARGUMENT_COUNT = 0; | |
| /** | |
| * @var string | |
| */ | |
| protected $name; | |
| /** | |
| * @var string | |
| */ | |
| protected $value; | |
| /** | |
| * @var array | |
| */ | |
| protected $params; | |
| /** | |
| * @var string | |
| */ | |
| protected $description; | |
| function __construct($value) | |
| { | |
| $this->value = $value; | |
| if (static::ARGUMENT_COUNT > 0) { | |
| $maxParams = static::ARGUMENT_COUNT + 1; | |
| $this->params = explode(static::ARGUMENT_SEPARATOR, $value, $maxParams); | |
| if (count($this->params) == $maxParams) { | |
| $this->description = array_pop($this->params); | |
| } | |
| } else { | |
| $this->description = $value; | |
| } | |
| } | |
| public function getName() | |
| { | |
| return $this->name; | |
| } | |
| public function getValue() | |
| { | |
| return $this->value; | |
| } | |
| public function getParameters() | |
| { | |
| return $this->params; | |
| } | |
| public function getDescription() | |
| { | |
| return $this->description; | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment