classname = ltrim($class, '\\'); $this->cache = $cache; #if ( ! $this->cache || ! $this->cache->has($class) ) { $this->classReflection = $class instanceof ReflectionClass ? $class : new ReflectionClass($class); $this->annotationReader = $annotationReader ?: AnnotationReader::fromClass($class); # } } public static function fromClass($class, ? CacheInterface $cache = null) : self { return new static($class, null, $cache); } public function read(bool $fullUses = true, bool $fullObject = true, $fullMethod = true, $fullProperty = true) : array { return $this->handleCaching(implode('', [ $this->classname, (int)$fullObject, (int) $fullMethod, (int) $fullProperty ]), fn() => [ 'uses' => $this->gatherUses($fullUses), 'class' => $this->gatherClass($fullObject), 'method' => $this->gatherMethods($fullMethod), 'property' => $this->gatherProperties($fullProperty), ]); } public function gatherUses(bool $full = true) : array { if ( $full ) { if ( $parentClass = $this->classReflection->getParentClass() ) { $list = static::fromClass($parentClass)->gatherUses(true); } foreach($this->classReflection->getTraits() as $trait) { $list = array_replace(static::fromClass($trait->name)->gatherUses(true), $list ?? []); } } return array_replace($list ?? [], $this->getUsesStatements()); } public function gatherClass(bool $full = true) : array { if ( $full ) { if ( $parentClass = $this->classReflection->getParentClass() ) { $class = static::fromClass($parentClass)->gatherClass(true); } if ( $traits = $this->classReflection->getTraits() ) { foreach($traits as $key => $value) { $traitTags = static::fromClass($key)->gatherClass(true); } } if ( $interfaces = $this->classReflection->getInterfaces() ) { foreach($interfaces as $key => $value) { $interfaceTags = static::fromClass($key)->gatherClass(true); } } $itemName = function($item) { return $item->getName(); }; } return array_merge_recursive($class ?? [], $traitTags ?? [], $interfaceTags ?? [], [ 'tags' => $this->annotationReader->getClass($this->classReflection) ] + ( ! $full ? [] : [ 'traits' => array_map($itemName, $traits), 'interfaces' => array_map($itemName, $interfaces), ] )); } public function gatherProperties(bool $full = true, int $filter = ReflectionProperty::IS_PUBLIC | ReflectionProperty::IS_PROTECTED | ReflectionProperty::IS_PRIVATE ) : array { $defaultValues = $this->classReflection->getDefaultProperties(); if ( $full ) { if ( $parentClass = $this->classReflection->getParentClass() ) { $properties = static::fromClass($parentClass)->gatherProperties($full, $filter); } } $list = []; foreach($this->classReflection->getProperties($filter) 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['builtin'] = $property->getType()->isBuiltIn(); $current['nullable'] = $property->getType()->allowsNull(); } $current['tags'] = $this->annotationReader->getProperty($property); if ( $this->ignoreElementAnnotation($current['tags']) ) { continue; } $list[ $current['name'] ] = $current; } return array_merge($properties ?? [], $list); } public function gatherMethods(bool $full = true, int $filter = ReflectionMethod::IS_PUBLIC | ReflectionMethod::IS_PROTECTED | ReflectionMethod::IS_PRIVATE | ReflectionMethod::IS_STATIC ) : array { $list = []; if ( $full ) { if ( $parentClass = $this->classReflection->getParentClass() ) { $methods = static::fromClass($parentClass)->gatherMethods($full, $filter); } } foreach($this->classReflection->getMethods($filter) as $method) { if ( ! $full && ( $method->class !== $this->classname ) ) { continue; } $parameters = []; foreach($method->getParameters() as $parameter) { $parameters[$parameter->getName()] = [ 'null' => $parameter->allowsNull(), 'position' => $parameter->getPosition(), 'type' => $parameter->hasType() && $parameter->getType() instanceof \ReflectionNamedType ? $parameter->getType()->getName() : false, 'array' => $this->isType('array', $parameter), 'callable' => $this->isType('callable', $parameter), 'optional' => $parameter->isOptional(), 'byReference' => $parameter->isPassedByReference(), ]; } $current = [ 'name' => $method->getName(), 'type' => $method->hasReturnType() && $method->getReturnType() instanceof \ReflectionNamedType ? $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 array_merge($methods ?? [], $list); } protected function ignoreElementAnnotation($tags) : bool { return [] !== array_filter($tags, fn($e) => ( $e['object'] ?? null ) instanceof Ignore); } protected function readCode() : string { static $code = []; $fileName = $this->classReflection->getFilename(); return $code[$fileName] ?? $code[$fileName] = file_get_contents($fileName); } # From https://www.php.net/manual/en/reflectionparameter.isarray.php public static function isType(string $type, ReflectionParameter $reflectionParameter) : bool { if ( $reflectionType = $reflectionParameter->getType() ) { $types = $reflectionType instanceof ReflectionUnionType ? $reflectionType->getTypes() : [$reflectionType]; return in_array($type, array_map(fn(ReflectionNamedType $t) => $t->getName(), $types)); } return false; } 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_NAME_QUALIFIED: 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 ) { $clsName = ltrim(substr($latestString, strrpos($latestString, "\\") ), '\\'); if ( $replacedClass ) { $uses[$replacedClass] = $clsName; $replacedClass = ""; } elseif ( $latestString ) { $uses[implode("\\", array_merge($statement, [ $latestString ]))] = $clsName; } } 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; } }