Extractor.php 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550
  1. <?php
  2. namespace app\admin\command\Api\library;
  3. /**
  4. * Class imported from https://github.com/eriknyk/Annotations
  5. * @author Erik Amaru Ortiz https://github.com/eriknyk‎
  6. *
  7. * @license http://opensource.org/licenses/bsd-license.php The BSD License
  8. * @author Calin Rada <rada.calin@gmail.com>
  9. */
  10. class Extractor
  11. {
  12. /**
  13. * Static array to store already parsed annotations
  14. * @var array
  15. */
  16. private static $annotationCache;
  17. /**
  18. * Indicates that annotations should has strict behavior, 'false' by default
  19. * @var boolean
  20. */
  21. private $strict = false;
  22. /**
  23. * Stores the default namespace for Objects instance, usually used on methods like getMethodAnnotationsObjects()
  24. * @var string
  25. */
  26. public $defaultNamespace = '';
  27. /**
  28. * Sets strict variable to true/false
  29. * @param bool $value boolean value to indicate that annotations to has strict behavior
  30. */
  31. public function setStrict($value)
  32. {
  33. $this->strict = (bool) $value;
  34. }
  35. /**
  36. * Sets default namespace to use in object instantiation
  37. * @param string $namespace default namespace
  38. */
  39. public function setDefaultNamespace($namespace)
  40. {
  41. $this->defaultNamespace = $namespace;
  42. }
  43. /**
  44. * Gets default namespace used in object instantiation
  45. * @return string $namespace default namespace
  46. */
  47. public function getDefaultAnnotationNamespace()
  48. {
  49. return $this->defaultNamespace;
  50. }
  51. /**
  52. * Gets all anotations with pattern @SomeAnnotation() from a given class
  53. *
  54. * @param string $className class name to get annotations
  55. * @return array self::$annotationCache all annotated elements
  56. */
  57. public static function getClassAnnotations($className)
  58. {
  59. if (!isset(self::$annotationCache[$className]))
  60. {
  61. $class = new \ReflectionClass($className);
  62. self::$annotationCache[$className] = self::parseAnnotations($class->getDocComment());
  63. }
  64. return self::$annotationCache[$className];
  65. }
  66. public static function getAllClassAnnotations($className)
  67. {
  68. $class = new \ReflectionClass($className);
  69. foreach ($class->getMethods() as $object)
  70. {
  71. self::$annotationCache['annotations'][$className][$object->name] = self::getMethodAnnotations($className, $object->name);
  72. }
  73. return self::$annotationCache['annotations'];
  74. }
  75. /**
  76. * Gets all anotations with pattern @SomeAnnotation() from a determinated method of a given class
  77. *
  78. * @param string $className class name
  79. * @param string $methodName method name to get annotations
  80. * @return array self::$annotationCache all annotated elements of a method given
  81. */
  82. public static function getMethodAnnotations($className, $methodName)
  83. {
  84. if (!isset(self::$annotationCache[$className . '::' . $methodName]))
  85. {
  86. try
  87. {
  88. $method = new \ReflectionMethod($className, $methodName);
  89. $class = new \ReflectionClass($className);
  90. if (!$method->isPublic() || $method->isConstructor())
  91. {
  92. $annotations = array();
  93. }
  94. else
  95. {
  96. $annotations = self::consolidateAnnotations($method, $class);
  97. }
  98. }
  99. catch (\ReflectionException $e)
  100. {
  101. $annotations = array();
  102. }
  103. self::$annotationCache[$className . '::' . $methodName] = $annotations;
  104. }
  105. return self::$annotationCache[$className . '::' . $methodName];
  106. }
  107. /**
  108. * Gets all anotations with pattern @SomeAnnotation() from a determinated method of a given class
  109. * and instance its abcAnnotation class
  110. *
  111. * @param string $className class name
  112. * @param string $methodName method name to get annotations
  113. * @return array self::$annotationCache all annotated objects of a method given
  114. */
  115. public function getMethodAnnotationsObjects($className, $methodName)
  116. {
  117. $annotations = $this->getMethodAnnotations($className, $methodName);
  118. $objects = array();
  119. $i = 0;
  120. foreach ($annotations as $annotationClass => $listParams)
  121. {
  122. $annotationClass = ucfirst($annotationClass);
  123. $class = $this->defaultNamespace . $annotationClass . 'Annotation';
  124. // verify is the annotation class exists, depending if Annotations::strict is true
  125. // if not, just skip the annotation instance creation.
  126. if (!class_exists($class))
  127. {
  128. if ($this->strict)
  129. {
  130. throw new Exception(sprintf('Runtime Error: Annotation Class Not Found: %s', $class));
  131. }
  132. else
  133. {
  134. // silent skip & continue
  135. continue;
  136. }
  137. }
  138. if (empty($objects[$annotationClass]))
  139. {
  140. $objects[$annotationClass] = new $class();
  141. }
  142. foreach ($listParams as $params)
  143. {
  144. if (is_array($params))
  145. {
  146. foreach ($params as $key => $value)
  147. {
  148. $objects[$annotationClass]->set($key, $value);
  149. }
  150. }
  151. else
  152. {
  153. $objects[$annotationClass]->set($i++, $params);
  154. }
  155. }
  156. }
  157. return $objects;
  158. }
  159. private static function consolidateAnnotations($method, $class)
  160. {
  161. $dockblockClass = $class->getDocComment();
  162. $docblockMethod = $method->getDocComment();
  163. $methodName = $method->getName();
  164. $methodAnnotations = self::parseAnnotations($docblockMethod);
  165. $classAnnotations = self::parseAnnotations($dockblockClass);
  166. if (isset($methodAnnotations['ApiInternal']) || $methodName == '_initialize' || $methodName == '_empty')
  167. {
  168. return [];
  169. }
  170. $properties = $class->getDefaultProperties();
  171. $noNeedLogin = isset($properties['noNeedLogin']) ? is_array($properties['noNeedLogin']) ? $properties['noNeedLogin'] : [$properties['noNeedLogin']] : [];
  172. $noNeedRight = isset($properties['noNeedRight']) ? is_array($properties['noNeedRight']) ? $properties['noNeedRight'] : [$properties['noNeedRight']] : [];
  173. preg_match_all("/\*[\s]+(.*)(\\r\\n|\\r|\\n)/U", str_replace('/**', '', $docblockMethod), $methodArr);
  174. preg_match_all("/\*[\s]+(.*)(\\r\\n|\\r|\\n)/U", str_replace('/**', '', $dockblockClass), $classArr);
  175. $methodTitle = isset($methodArr[1]) && isset($methodArr[1][0]) ? $methodArr[1][0] : '';
  176. $classTitle = isset($classArr[1]) && isset($classArr[1][0]) ? $classArr[1][0] : '';
  177. if (!isset($methodAnnotations['ApiMethod']))
  178. {
  179. $methodAnnotations['ApiMethod'] = ['get'];
  180. }
  181. if (!isset($methodAnnotations['ApiSummary']))
  182. {
  183. $methodAnnotations['ApiSummary'] = [$methodTitle];
  184. }
  185. if ($methodAnnotations)
  186. {
  187. foreach ($classAnnotations as $name => $valueClass)
  188. {
  189. if (count($valueClass) !== 1)
  190. {
  191. continue;
  192. }
  193. if ($name === 'ApiRoute')
  194. {
  195. if (isset($methodAnnotations[$name]))
  196. {
  197. $methodAnnotations[$name] = [rtrim($valueClass[0], '/') . $methodAnnotations[$name][0]];
  198. }
  199. else
  200. {
  201. $methodAnnotations[$name] = [rtrim($valueClass[0], '/') . '/' . $method->getName()];
  202. }
  203. }
  204. if ($name === 'ApiSector')
  205. {
  206. $methodAnnotations[$name] = $valueClass;
  207. }
  208. }
  209. }
  210. if (!isset($methodAnnotations['ApiTitle']))
  211. {
  212. $methodAnnotations['ApiTitle'] = [$methodTitle];
  213. }
  214. if (!isset($methodAnnotations['ApiRoute']))
  215. {
  216. $urlArr = [];
  217. $className = $class->getName();
  218. list($prefix, $suffix) = explode('\\' . \think\Config::get('url_controller_layer') . '\\', $className);
  219. $prefixArr = explode('\\', $prefix);
  220. $suffixArr = explode('\\', $suffix);
  221. if ($prefixArr[0] == \think\Config::get('app_namespace'))
  222. {
  223. $prefixArr[0] = '';
  224. }
  225. $urlArr = array_merge($urlArr, $prefixArr);
  226. $urlArr[] = implode('.', array_map(function($item) {
  227. return \think\Loader::parseName($item);
  228. }, $suffixArr));
  229. $urlArr[] = $method->getName();
  230. $methodAnnotations['ApiRoute'] = [implode('/', $urlArr)];
  231. }
  232. if (!isset($methodAnnotations['ApiSector']))
  233. {
  234. $methodAnnotations['ApiSector'] = isset($classAnnotations['ApiSector']) ? $classAnnotations['ApiSector'] : [$classTitle];
  235. }
  236. if (!isset($methodAnnotations['ApiParams']))
  237. {
  238. $params = self::parseCustomAnnotations($docblockMethod, 'param');
  239. foreach ($params as $k => $v)
  240. {
  241. $arr = explode(' ', preg_replace("/[\s]+/", " ", $v));
  242. $methodAnnotations['ApiParams'][] = [
  243. 'name' => isset($arr[1]) ? str_replace('$', '', $arr[1]) : '',
  244. 'nullable' => false,
  245. 'type' => isset($arr[0]) ? $arr[0] : 'string',
  246. 'description' => isset($arr[2]) ? $arr[2] : ''
  247. ];
  248. }
  249. }
  250. $methodAnnotations['ApiPermissionLogin'] = [!in_array('*', $noNeedLogin) && !in_array($methodName, $noNeedLogin)];
  251. $methodAnnotations['ApiPermissionRight'] = [!in_array('*', $noNeedRight) && !in_array($methodName, $noNeedRight)];
  252. return $methodAnnotations;
  253. }
  254. /**
  255. * Parse annotations
  256. *
  257. * @param string $docblock
  258. * @param string $name
  259. * @return array parsed annotations params
  260. */
  261. private static function parseCustomAnnotations($docblock, $name = 'param')
  262. {
  263. $annotations = array();
  264. $docblock = substr($docblock, 3, -2);
  265. if (preg_match_all('/@' . $name . '(?:\s*(?:\(\s*)?(.*?)(?:\s*\))?)??\s*(?:\n|\*\/)/', $docblock, $matches))
  266. {
  267. foreach ($matches[1] as $k => $v)
  268. {
  269. $annotations[] = $v;
  270. }
  271. }
  272. return $annotations;
  273. }
  274. /**
  275. * Parse annotations
  276. *
  277. * @param string $docblock
  278. * @return array parsed annotations params
  279. */
  280. private static function parseAnnotations($docblock)
  281. {
  282. $annotations = array();
  283. // Strip away the docblock header and footer to ease parsing of one line annotations
  284. $docblock = substr($docblock, 3, -2);
  285. if (preg_match_all('/@(?<name>[A-Za-z_-]+)[\s\t]*\((?<args>(?:(?!\)).)*)\)\r?/s', $docblock, $matches))
  286. {
  287. $numMatches = count($matches[0]);
  288. for ($i = 0; $i < $numMatches; ++$i)
  289. {
  290. // annotations has arguments
  291. if (isset($matches['args'][$i]))
  292. {
  293. $argsParts = trim($matches['args'][$i]);
  294. $name = $matches['name'][$i];
  295. $value = self::parseArgs($argsParts);
  296. }
  297. else
  298. {
  299. $value = array();
  300. }
  301. $annotations[$name][] = $value;
  302. }
  303. }
  304. return $annotations;
  305. }
  306. /**
  307. * Parse individual annotation arguments
  308. *
  309. * @param string $content arguments string
  310. * @return array annotated arguments
  311. */
  312. private static function parseArgs($content)
  313. {
  314. // Replace initial stars
  315. $content = preg_replace('/^\s*\*/m', '', $content);
  316. $data = array();
  317. $len = strlen($content);
  318. $i = 0;
  319. $var = '';
  320. $val = '';
  321. $level = 1;
  322. $prevDelimiter = '';
  323. $nextDelimiter = '';
  324. $nextToken = '';
  325. $composing = false;
  326. $type = 'plain';
  327. $delimiter = null;
  328. $quoted = false;
  329. $tokens = array('"', '"', '{', '}', ',', '=');
  330. while ($i <= $len)
  331. {
  332. $prev_c = substr($content, $i - 1, 1);
  333. $c = substr($content, $i++, 1);
  334. if ($c === '"' && $prev_c !== "\\")
  335. {
  336. $delimiter = $c;
  337. //open delimiter
  338. if (!$composing && empty($prevDelimiter) && empty($nextDelimiter))
  339. {
  340. $prevDelimiter = $nextDelimiter = $delimiter;
  341. $val = '';
  342. $composing = true;
  343. $quoted = true;
  344. }
  345. else
  346. {
  347. // close delimiter
  348. if ($c !== $nextDelimiter)
  349. {
  350. throw new Exception(sprintf(
  351. "Parse Error: enclosing error -> expected: [%s], given: [%s]", $nextDelimiter, $c
  352. ));
  353. }
  354. // validating syntax
  355. if ($i < $len)
  356. {
  357. if (',' !== substr($content, $i, 1) && '\\' !== $prev_c)
  358. {
  359. throw new Exception(sprintf(
  360. "Parse Error: missing comma separator near: ...%s<--", substr($content, ($i - 10), $i)
  361. ));
  362. }
  363. }
  364. $prevDelimiter = $nextDelimiter = '';
  365. $composing = false;
  366. $delimiter = null;
  367. }
  368. }
  369. elseif (!$composing && in_array($c, $tokens))
  370. {
  371. switch ($c)
  372. {
  373. case '=':
  374. $prevDelimiter = $nextDelimiter = '';
  375. $level = 2;
  376. $composing = false;
  377. $type = 'assoc';
  378. $quoted = false;
  379. break;
  380. case ',':
  381. $level = 3;
  382. // If composing flag is true yet,
  383. // it means that the string was not enclosed, so it is parsing error.
  384. if ($composing === true && !empty($prevDelimiter) && !empty($nextDelimiter))
  385. {
  386. throw new Exception(sprintf(
  387. "Parse Error: enclosing error -> expected: [%s], given: [%s]", $nextDelimiter, $c
  388. ));
  389. }
  390. $prevDelimiter = $nextDelimiter = '';
  391. break;
  392. case '{':
  393. $subc = '';
  394. $subComposing = true;
  395. while ($i <= $len)
  396. {
  397. $c = substr($content, $i++, 1);
  398. if (isset($delimiter) && $c === $delimiter)
  399. {
  400. throw new Exception(sprintf(
  401. "Parse Error: Composite variable is not enclosed correctly."
  402. ));
  403. }
  404. if ($c === '}')
  405. {
  406. $subComposing = false;
  407. break;
  408. }
  409. $subc .= $c;
  410. }
  411. // if the string is composing yet means that the structure of var. never was enclosed with '}'
  412. if ($subComposing)
  413. {
  414. throw new Exception(sprintf(
  415. "Parse Error: Composite variable is not enclosed correctly. near: ...%s'", $subc
  416. ));
  417. }
  418. $val = self::parseArgs($subc);
  419. break;
  420. }
  421. }
  422. else
  423. {
  424. if ($level == 1)
  425. {
  426. $var .= $c;
  427. }
  428. elseif ($level == 2)
  429. {
  430. $val .= $c;
  431. }
  432. }
  433. if ($level === 3 || $i === $len)
  434. {
  435. if ($type == 'plain' && $i === $len)
  436. {
  437. $data = self::castValue($var);
  438. }
  439. else
  440. {
  441. $data[trim($var)] = self::castValue($val, !$quoted);
  442. }
  443. $level = 1;
  444. $var = $val = '';
  445. $composing = false;
  446. $quoted = false;
  447. }
  448. }
  449. return $data;
  450. }
  451. /**
  452. * Try determinate the original type variable of a string
  453. *
  454. * @param string $val string containing possibles variables that can be cast to bool or int
  455. * @param boolean $trim indicate if the value passed should be trimmed after to try cast
  456. * @return mixed returns the value converted to original type if was possible
  457. */
  458. private static function castValue($val, $trim = false)
  459. {
  460. if (is_array($val))
  461. {
  462. foreach ($val as $key => $value)
  463. {
  464. $val[$key] = self::castValue($value);
  465. }
  466. }
  467. elseif (is_string($val))
  468. {
  469. if ($trim)
  470. {
  471. $val = trim($val);
  472. }
  473. $val = stripslashes($val);
  474. $tmp = strtolower($val);
  475. if ($tmp === 'false' || $tmp === 'true')
  476. {
  477. $val = $tmp === 'true';
  478. }
  479. elseif (is_numeric($val))
  480. {
  481. return $val + 0;
  482. }
  483. unset($tmp);
  484. }
  485. return $val;
  486. }
  487. }