commit 504c87b134ace8daba82185cc08992d723d67872 Author: Dave Mc Nicoll Date: Fri Oct 4 15:48:21 2019 +0000 - First commit -- splitted from Ulmus, but still untested diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..cb3012e --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2019 Dave Mc Nicoll + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..3fcbd8c --- /dev/null +++ b/composer.json @@ -0,0 +1,18 @@ +{ + "name": "mcnd/notes", + "description": "Easy annotation based on PHP array syntax", + "type": "library", + "license": "MIT", + "authors": [ + { + "name": "Dave Mc Nicoll", + "email": "mcndave@gmail.com" + } + ], + "require": {}, + "autoload": { + "psr-4": { + "Notes\\": "src/" + } + } +} diff --git a/src/Annotation.php b/src/Annotation.php new file mode 100644 index 0000000..47538f3 --- /dev/null +++ b/src/Annotation.php @@ -0,0 +1,5 @@ +class = $class; + } + + public static function fromClass($class) : self + { + return new static($class); + } + + public function getProperty(ReflectionProperty $property) + { + return $this->parseDocComment($property); + } + + public function getClass(ReflectionClass $class) + { + return $this->parseDocComment($class); + } + + public function getMethod(ReflectionMethod $method) + { + return $this->parseDocComment($method); + } + + protected function parseDocComment(Reflector $reflect) + { + $namespace = $this->getObjectNamespace($reflect); + $tags = []; + + foreach(preg_split("/\r\n|\n|\r/", $reflect->getDocComment()) as $line) { + $line = ltrim($line, "* \t\/"); + $line = rtrim($line, "\t "); + + if ( substr($line, 0, 1) === '@' ) { + $line = ltrim($line, '@'); + + $open = strpos($line, "("); + $close = strrpos($line, ")"); + + if ( ! in_array(false, [ $open, $close ], true) && ( ++$open !== $close ) ) { + $arguments = substr($line, $open, $close - $open); + + try { + $tags[] = [ + 'tag' => substr($line, 0, $open - 1), + 'arguments' => eval("namespace $namespace; return [ $arguments ];"), + ]; + } + catch(\Throwable $error) { + throw new \InvalidArgumentException("An error occured while parsing annotation from '" . $this->getObjectName($reflect) . "' : @$line -- " . $error->getMessage()); + } + } + else { + $tags[] = [ + 'tag' => $line, + 'arguments' => [], + ]; + } + } + } + + return $tags; + } + + protected function getObjectName(Reflector $reflect) : string + { + switch(true) { + case $reflect instanceof ReflectionMethod : + case $reflect instanceof ReflectionProperty : + return $reflect->class . "::" . $reflect->name; + + case $reflect instanceof ReflectionClass : + return $reflect->name; + } + } + + protected function getObjectNamespace(Reflector $reflect) : string + { + switch(true) { + case $reflect instanceof ReflectionMethod : + case $reflect instanceof ReflectionProperty : + return $reflect->getDeclaringClass()->getNamespaceName(); + + case $reflect instanceof ReflectionClass : + return $reflect->getNamespaceName(); + } + } +} diff --git a/src/ObjectReflection.php b/src/ObjectReflection.php new file mode 100644 index 0000000..5f1c67c --- /dev/null +++ b/src/ObjectReflection.php @@ -0,0 +1,267 @@ +classReflection = $class instanceof ReflectionClass ? $class : new ReflectionClass($class); + $this->annotationReader = $annotationReader ?: AnnotationReader::fromClass($class); + } + + public static function fromClass($class) : self + { + return new static($class); + } + + public function read() : array + { + return [ + 'uses' => $this->gatherUses(true), + 'class' => $this->gatherClass(true), + 'method' => $this->gatherMethods(true), + 'property' => $this->gatherProperties(true), + ]; + } + + public function gatherUses(bool $full = true) : array + { + $list = []; + + if ( $full ) { + if ( $parentClass = $this->classReflection->getParentClass() ) { + $list = static::fromClass($parentClass)->gatherUses(true); + } + + foreach($this->classReflection->getTraits() as $trait) { + $list = array_merge($list, static::fromClass($trait)->gatherUses(true)); + } + } + + return array_merge($this->getUsesStatements(), $list); + } + + public function gatherClass(bool $full = true) : array + { + $class = []; + + if ( $full ) { + if ( $parentClass = $this->classReflection->getParentClass() ) { + $class = static::fromClass($parentClass)->gatherClass(true); + } + + $itemName = function($item) { + return $item->getName(); + }; + } + + return [ + 'tags' => array_merge($class, $this->annotationReader->getClass($this->classReflection)) + ] + ( ! $full ? [] : [ + 'traits' => array_map($itemName, $this->classReflection->getTraits()), + 'interfaces' => array_map($itemName, $this->classReflection->getInterfaces()), + ]); + } + + public function gatherProperties(bool $full = true, int $filter = + ReflectionProperty::IS_PUBLIC | + ReflectionProperty::IS_PROTECTED | + ReflectionProperty::IS_PRIVATE + ) : array + { + $properties = []; + $defaultValues = $this->classReflection->getDefaultProperties(); + + if ( $full ) { + if ( $parentClass = $this->classReflection->getParentClass() ) { + $properties = static::fromClass($parentClass)->gatherProperties($full, $filter); + } + } + + $properties = array_merge($properties, $this->classReflection->getProperties($filter)); + + $list = []; + + foreach($properties as $property) { + $current = [ + 'name' => $property->getName() + ]; + + # Default value can be 'null', so isset() it not suitable here + if ( array_key_exists($current['name'], $defaultValues) ) { + $current['value'] = $defaultValues[ $current['name'] ]; + } + + if ( $property->hasType() ) { + $current['type'] = $property->getType()->getName(); + $current['nullable'] = $property->getType()->allowsNull(); + } + + $current['tags'] = $this->annotationReader->getProperty($property); + + if ( $this->ignoreElementAnnotation($current['tags']) ) { + continue; + } + + $list[ $current['name'] ] = $current; + } + + return $list; + } + + public function gatherMethods(bool $full = true, int $filter = + ReflectionMethod::IS_PUBLIC | + ReflectionMethod::IS_PROTECTED | + ReflectionMethod::IS_PRIVATE | + ReflectionMethod::IS_STATIC + ) : array + { + $list = $methods = []; + + if ( $full ) { + if ( $parentClass = $this->classReflection->getParentClass() ) { + $methods = static::fromClass($parentClass)->gatherMethods($full, $filter); + } + } + + $methods = array_merge($methods, $this->classReflection->getMethods($filter)); + + foreach($methods as $method) { + $parameters = []; + + foreach($method->getParameters() as $parameter) { + $parameters[$parameter->getName()] = [ + 'null' => $parameter->allowsNull(), + 'position' => $parameter->getPosition(), + 'type' => $parameter->hasType() ? $parameter->getType()->getName() : false, + 'array' => $parameter->isArray(), + 'callable' => $parameter->isCallable(), + 'optional' => $parameter->isOptional(), + 'byReference' => $parameter->isPassedByReference(), + ]; + } + + $current = [ + 'name' => $method->getName(), + 'type' => $method->hasReturnType() ? $method->getReturnType()->getName() : false, + 'constructor' => $method->isConstructor(), + 'destructor' => $method->isDestructor(), + 'parameters' => $parameters, + ]; + + $current['tags'] = $this->annotationReader->getMethod($method); + + if ( $this->ignoreElementAnnotation($current['tags']) ) { + continue; + } + + $list[ $current['name'] ] = $current; + } + + return $list; + } + + protected function ignoreElementAnnotation($tags) : bool + { + return in_array('IGNORE', array_map('strtoupper', array_column($tags, 'tag') )); + } + + + protected function readCode() : string + { + static $code = []; + $fileName = $this->classReflection->getFilename(); + return $code[$fileName] ?? $code[$fileName] = file_get_contents($fileName); + } + + protected function getUsesStatements() : array + { + $uses = []; + $tokens = token_get_all( $c = $this->readCode() ); + + while ( $token = array_shift($tokens) ) { + + if ( is_array($token) ) { + list($token, $value) = $token; + } + + switch ($token) { + case T_CLASS: + case T_TRAIT: + case T_INTERFACE: + break 2; + + case T_USE: + $isUse = true; + break; + + case T_NS_SEPARATOR: + $isNamespace = $isUse; + break; + + case T_STRING: + if ( $isNamespace && $latestString ) { + $statement[] = $latestString; + } + + $latestString = $value; + break; + + case T_AS: + # My\Name\Space\aClassHere `as` ClassAlias; + $replacedClass = implode("\\", array_merge($statement, [ $latestString ])); + $latestString = null; + break; + + case T_WHITESPACE: + case T_COMMENT: + case T_DOC_COMMENT: + break; + + case '{': + # opening a sub-namespace -> \My\Name\Space\`{`OneItem, AnotherItem} + if ( $isNamespace ) { + $inNamespace = true; + } + break; + + case ';'; + case ',': + case '}': + if ( $isUse ) { + if ( $replacedClass ) { + $uses[$replacedClass] = $latestString; + $replacedClass = ""; + } + elseif ( $latestString ) { + $uses[implode("\\", array_merge($statement, [ $latestString ]))] = $latestString; + } + } + + if ( $inNamespace ) { + $latestString = ""; + + # \My\Name\Space\{OneItem, AnotherItem`}` <- closing a sub-namespace + if ( $token !== "}" ) { + break; + } + } + + case T_OPEN_TAG: + default: + $statement = []; + $latestString = ""; + $replacedClass = null; + $isNamespace = $inNamespace = false; + $isUse = ( $isUse ?? false ) && ( $token === ',' ); + break; + } + } + + return $uses; + } +}