You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

187 lines
5.0 KiB

  1. <?php
  2. namespace dokuwiki\Remote\OpenApiDoc;
  3. class ClassResolver
  4. {
  5. /** @var ClassResolver */
  6. private static $instance;
  7. protected $classUses = [];
  8. protected $classDocs = [];
  9. /**
  10. * Get a singleton instance
  11. *
  12. * Constructor is public for testing purposes
  13. * @return ClassResolver
  14. */
  15. public static function getInstance()
  16. {
  17. if (self::$instance === null) {
  18. self::$instance = new self();
  19. }
  20. return self::$instance;
  21. }
  22. /**
  23. * Resolve a class name to a fully qualified class name
  24. *
  25. * Results are cached in the instance for reuse
  26. *
  27. * @param string $classalias The class name to resolve
  28. * @param string $context The classname in which context in which the class is used
  29. * @return string No guarantee that the class exists! No leading backslash!
  30. */
  31. public function resolve($classalias, $context)
  32. {
  33. if ($classalias[0] === '\\') {
  34. // Fully qualified class name given
  35. return ltrim($classalias, '\\');
  36. }
  37. $classinfo = $this->getClassUses($context);
  38. return $classinfo['uses'][$classalias] ?? $classinfo['ownNS'] . '\\' . $classalias;
  39. }
  40. /**
  41. * Resolve a class name to a fully qualified class name and return a DocBlockClass for it
  42. *
  43. * Results are cached in the instance for reuse
  44. *
  45. * @param string $classalias The class name to resolve
  46. * @param string $context The classname in which context in which the class is used
  47. * @return DocBlockClass|null
  48. */
  49. public function document($classalias, $context)
  50. {
  51. $class = $this->resolve($classalias, $context);
  52. if (!class_exists($class)) return null;
  53. if (isset($this->classDocs[$class])) {
  54. $reflector = new \ReflectionClass($class);
  55. $this->classDocs[$class] = new DocBlockClass($reflector);
  56. }
  57. return $this->classDocs[$class];
  58. }
  59. /**
  60. * Cached fetching of all defined class aliases
  61. *
  62. * @param string $class The class to parse
  63. * @return array
  64. */
  65. public function getClassUses($class)
  66. {
  67. if (!isset($this->classUses[$class])) {
  68. $reflector = new \ReflectionClass($class);
  69. $source = $this->readSource($reflector->getFileName(), $reflector->getStartLine());
  70. $this->classUses[$class] = [
  71. 'ownNS' => $reflector->getNamespaceName(),
  72. 'uses' => $this->tokenizeSource($source)
  73. ];
  74. }
  75. return $this->classUses[$class];
  76. }
  77. /**
  78. * Parse the use statements from the given source code
  79. *
  80. * This is a simplified version of the code by @jasondmoss - we do not support multiple
  81. * classed within one file
  82. *
  83. * @link https://gist.github.com/jasondmoss/6200807
  84. * @param string $source
  85. * @return array
  86. */
  87. private function tokenizeSource($source)
  88. {
  89. $tokens = token_get_all($source);
  90. $useStatements = [];
  91. $record = false;
  92. $currentUse = [
  93. 'class' => '',
  94. 'as' => ''
  95. ];
  96. foreach ($tokens as $token) {
  97. if (!is_array($token)) {
  98. // statement ended
  99. if ($record) {
  100. $useStatements[] = $currentUse;
  101. $record = false;
  102. $currentUse = [
  103. 'class' => '',
  104. 'as' => ''
  105. ];
  106. }
  107. continue;
  108. }
  109. $tokenname = token_name($token[0]);
  110. if ($token[0] === T_CLASS) {
  111. break; // we reached the class itself, no need to parse further
  112. }
  113. if ($token[0] === T_USE) {
  114. $record = 'class';
  115. continue;
  116. }
  117. if ($token[0] === T_AS) {
  118. $record = 'as';
  119. continue;
  120. }
  121. if ($record) {
  122. switch ($token[0]) {
  123. case T_STRING:
  124. case T_NS_SEPARATOR:
  125. case defined('T_NAME_QUALIFIED') ? T_NAME_QUALIFIED : -1: // PHP 7.4 compatibility
  126. $currentUse[$record] .= $token[1];
  127. break;
  128. }
  129. }
  130. }
  131. // Return a lookup table alias to FQCN
  132. $table = [];
  133. foreach ($useStatements as $useStatement) {
  134. $class = $useStatement['class'];
  135. $alias = $useStatement['as'] ?: substr($class, strrpos($class, '\\') + 1);
  136. $table[$alias] = $class;
  137. }
  138. return $table;
  139. }
  140. /**
  141. * Read file source up to the line where our class is defined.
  142. *
  143. * @return string
  144. */
  145. protected function readSource($file, $startline)
  146. {
  147. $file = fopen($file, 'r');
  148. $line = 0;
  149. $source = '';
  150. while (!feof($file)) {
  151. ++$line;
  152. if ($line >= $startline) {
  153. break;
  154. }
  155. $source .= fgets($file);
  156. }
  157. fclose($file);
  158. return $source;
  159. }
  160. }