Extractor.php 18 KB

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