CommentParser.php 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522
  1. <?php
  2. namespace Luracast\Restler;
  3. use Exception;
  4. use Luracast\Restler\Data\Text;
  5. /**
  6. * Parses the PHPDoc comments for metadata. Inspired by `Documentor` code base.
  7. *
  8. * @category Framework
  9. * @package Restler
  10. * @subpackage Helper
  11. * @author R.Arul Kumaran <arul@luracast.com>
  12. * @copyright 2010 Luracast
  13. * @license http://www.opensource.org/licenses/lgpl-license.php LGPL
  14. * @link http://luracast.com/products/restler/
  15. *
  16. */
  17. class CommentParser
  18. {
  19. /**
  20. * name for the embedded data
  21. *
  22. * @var string
  23. */
  24. public static $embeddedDataName = 'properties';
  25. /**
  26. * Regular Expression pattern for finding the embedded data and extract
  27. * the inner information. It is used with preg_match.
  28. *
  29. * @var string
  30. */
  31. public static $embeddedDataPattern
  32. = '/```(\w*)[\s]*(([^`]*`{0,2}[^`]+)*)```/ms';
  33. /**
  34. * Pattern will have groups for the inner details of embedded data
  35. * this index is used to locate the data portion.
  36. *
  37. * @var int
  38. */
  39. public static $embeddedDataIndex = 2;
  40. /**
  41. * Delimiter used to split the array data.
  42. *
  43. * When the name portion is of the embedded data is blank auto detection
  44. * will be used and if URLEncodedFormat is detected as the data format
  45. * the character specified will be used as the delimiter to find split
  46. * array data.
  47. *
  48. * @var string
  49. */
  50. public static $arrayDelimiter = ',';
  51. /**
  52. * @var array annotations that support array value
  53. */
  54. public static $allowsArrayValue = array(
  55. 'choice' => true,
  56. 'select' => true,
  57. 'properties' => true,
  58. );
  59. /**
  60. * character sequence used to escape \@
  61. */
  62. const escapedAtChar = '\\@';
  63. /**
  64. * character sequence used to escape end of comment
  65. */
  66. const escapedCommendEnd = '{@*}';
  67. /**
  68. * Instance of Restler class injected at runtime.
  69. *
  70. * @var Restler
  71. */
  72. public $restler;
  73. /**
  74. * Comment information is parsed and stored in to this array.
  75. *
  76. * @var array
  77. */
  78. private $_data = array();
  79. /**
  80. * Parse the comment and extract the data.
  81. *
  82. * @static
  83. *
  84. * @param $comment
  85. * @param bool $isPhpDoc
  86. *
  87. * @return array associative array with the extracted values
  88. */
  89. public static function parse($comment, $isPhpDoc = true)
  90. {
  91. $p = new self();
  92. if (empty($comment)) {
  93. return $p->_data;
  94. }
  95. if ($isPhpDoc) {
  96. $comment = self::removeCommentTags($comment);
  97. }
  98. $p->extractData($comment);
  99. return $p->_data;
  100. }
  101. /**
  102. * Removes the comment tags from each line of the comment.
  103. *
  104. * @static
  105. *
  106. * @param string $comment PhpDoc style comment
  107. *
  108. * @return string comments with out the tags
  109. */
  110. public static function removeCommentTags($comment)
  111. {
  112. $pattern = '/(^\/\*\*)|(^\s*\**[ \/]?)|\s(?=@)|\s\*\//m';
  113. return preg_replace($pattern, '', $comment);
  114. }
  115. /**
  116. * Extracts description and long description, uses other methods to get
  117. * parameters.
  118. *
  119. * @param $comment
  120. *
  121. * @return array
  122. */
  123. private function extractData($comment)
  124. {
  125. //to use @ as part of comment we need to
  126. $comment = str_replace(
  127. array(self::escapedCommendEnd, self::escapedAtChar),
  128. array('*/', '@'),
  129. $comment);
  130. $description = array();
  131. $longDescription = array();
  132. $params = array();
  133. $mode = 0; // extract short description;
  134. $comments = preg_split("/(\r?\n)/", $comment);
  135. // remove first blank line;
  136. array_shift($comments);
  137. $addNewline = false;
  138. foreach ($comments as $line) {
  139. $line = trim($line);
  140. $newParam = false;
  141. if (empty($line)) {
  142. if ($mode == 0) {
  143. $mode++;
  144. } else {
  145. $addNewline = true;
  146. }
  147. continue;
  148. } elseif ($line[0] == '@') {
  149. $mode = 2;
  150. $newParam = true;
  151. }
  152. switch ($mode) {
  153. case 0 :
  154. $description[] = $line;
  155. if (count($description) > 3) {
  156. // if more than 3 lines take only first line
  157. $longDescription = $description;
  158. $description[] = array_shift($longDescription);
  159. $mode = 1;
  160. } elseif (substr($line, -1) == '.') {
  161. $mode = 1;
  162. }
  163. break;
  164. case 1 :
  165. if ($addNewline) {
  166. $line = ' ' . $line;
  167. }
  168. $longDescription[] = $line;
  169. break;
  170. case 2 :
  171. $newParam
  172. ? $params[] = $line
  173. : $params[count($params) - 1] .= ' ' . $line;
  174. }
  175. $addNewline = false;
  176. }
  177. $description = implode(' ', $description);
  178. $longDescription = implode(' ', $longDescription);
  179. $description = preg_replace('/\s+/msu', ' ', $description);
  180. $longDescription = preg_replace('/\s+/msu', ' ', $longDescription);
  181. list($description, $d1)
  182. = $this->parseEmbeddedData($description);
  183. list($longDescription, $d2)
  184. = $this->parseEmbeddedData($longDescription);
  185. $this->_data = compact('description', 'longDescription');
  186. $d2 += $d1;
  187. if (!empty($d2)) {
  188. $this->_data[self::$embeddedDataName] = $d2;
  189. }
  190. foreach ($params as $key => $line) {
  191. list(, $param, $value) = preg_split('/\@|\s/', $line, 3)
  192. + array('', '', '');
  193. list($value, $embedded) = $this->parseEmbeddedData($value);
  194. $value = array_filter(preg_split('/\s+/msu', $value), 'strlen');
  195. $this->parseParam($param, $value, $embedded);
  196. }
  197. return $this->_data;
  198. }
  199. /**
  200. * Parse parameters that begin with (at)
  201. *
  202. * @param $param
  203. * @param array $value
  204. * @param array $embedded
  205. */
  206. private function parseParam($param, array $value, array $embedded)
  207. {
  208. $data = &$this->_data;
  209. $allowMultiple = false;
  210. switch ($param) {
  211. case 'param' :
  212. case 'property' :
  213. case 'property-read' :
  214. case 'property-write' :
  215. $value = $this->formatParam($value);
  216. $allowMultiple = true;
  217. break;
  218. case 'var' :
  219. $value = $this->formatVar($value);
  220. break;
  221. case 'return' :
  222. $value = $this->formatReturn($value);
  223. break;
  224. case 'class' :
  225. $data = &$data[$param];
  226. list ($param, $value) = $this->formatClass($value);
  227. break;
  228. case 'access' :
  229. $value = reset($value);
  230. break;
  231. case 'expires' :
  232. case 'status' :
  233. $value = intval(reset($value));
  234. break;
  235. case 'throws' :
  236. $value = $this->formatThrows($value);
  237. $allowMultiple = true;
  238. break;
  239. case 'author':
  240. $value = $this->formatAuthor($value);
  241. $allowMultiple = true;
  242. break;
  243. case 'header' :
  244. case 'link':
  245. case 'example':
  246. case 'todo':
  247. $allowMultiple = true;
  248. //don't break, continue with code for default:
  249. default :
  250. $value = implode(' ', $value);
  251. }
  252. if (!empty($embedded)) {
  253. if (is_string($value)) {
  254. $value = array('description' => $value);
  255. }
  256. $value[self::$embeddedDataName] = $embedded;
  257. }
  258. if (empty($data[$param])) {
  259. if ($allowMultiple) {
  260. $data[$param] = array(
  261. $value
  262. );
  263. } else {
  264. $data[$param] = $value;
  265. }
  266. } elseif ($allowMultiple) {
  267. $data[$param][] = $value;
  268. } elseif ($param == 'param') {
  269. $arr = array(
  270. $data[$param],
  271. $value
  272. );
  273. $data[$param] = $arr;
  274. } else {
  275. if (!is_string($value) && isset($value[self::$embeddedDataName])
  276. && isset($data[$param][self::$embeddedDataName])
  277. ) {
  278. $value[self::$embeddedDataName]
  279. += $data[$param][self::$embeddedDataName];
  280. }
  281. if (!is_array($data[$param])) {
  282. $data[$param] = array('description' => (string) $data[$param]);
  283. }
  284. if (is_array($value)) {
  285. $data[$param] = $value + $data[$param];
  286. }
  287. }
  288. }
  289. /**
  290. * Parses the inline php doc comments and embedded data.
  291. *
  292. * @param $subject
  293. *
  294. * @return array
  295. * @throws Exception
  296. */
  297. private function parseEmbeddedData($subject)
  298. {
  299. $data = array();
  300. //parse {@pattern } tags specially
  301. while (preg_match('|(?s-m)({@pattern (/.+/[imsxuADSUXJ]*)})|', $subject, $matches)) {
  302. $subject = str_replace($matches[0], '', $subject);
  303. $data['pattern'] = $matches[2];
  304. }
  305. while (preg_match('/{@(\w+)\s?([^}]*)}/ms', $subject, $matches)) {
  306. $name = $matches[1];
  307. $value = $matches[2];
  308. $subject = str_replace($matches[0], '', $subject);
  309. if ($name == 'pattern') {
  310. throw new Exception('Inline pattern tag should follow {@pattern /REGEX_PATTERN_HERE/} format and can optionally include PCRE modifiers following the ending `/`');
  311. } elseif (isset(static::$allowsArrayValue[$name])) {
  312. $value = explode(static::$arrayDelimiter, $value);
  313. } elseif ($value == 'true' || $value == 'false') {
  314. $value = $value == 'true';
  315. } elseif ($value == '') {
  316. $value = true;
  317. } elseif ($name == 'required') {
  318. $value = explode(static::$arrayDelimiter, $value);
  319. }
  320. if (defined('Luracast\\Restler\\UI\\HtmlForm::'.$name)) {
  321. $value = constant($value);
  322. }
  323. $data[$name] = $value;
  324. }
  325. while (preg_match(self::$embeddedDataPattern, $subject, $matches)) {
  326. $subject = str_replace($matches[0], '', $subject);
  327. $str = $matches[self::$embeddedDataIndex];
  328. if (isset($this->restler)
  329. && self::$embeddedDataIndex > 1
  330. && !empty($name)
  331. ) {
  332. $extension = $name;
  333. $formatMap = $this->restler->getFormatMap();
  334. if (isset($formatMap[$extension])) {
  335. /**
  336. * @var \Luracast\Restler\Format\iFormat
  337. */
  338. $format = $formatMap[$extension];
  339. $format = new $format();
  340. $data = $format->decode($str);
  341. }
  342. } else { // auto detect
  343. if ($str[0] == '{') {
  344. $d = json_decode($str, true);
  345. if (json_last_error() != JSON_ERROR_NONE) {
  346. throw new Exception('Error parsing embedded JSON data'
  347. . " $str");
  348. }
  349. $data = $d + $data;
  350. } else {
  351. parse_str($str, $d);
  352. //clean up
  353. $d = array_filter($d);
  354. foreach ($d as $key => $val) {
  355. $kt = trim($key);
  356. if ($kt != $key) {
  357. unset($d[$key]);
  358. $key = $kt;
  359. $d[$key] = $val;
  360. }
  361. if (is_string($val)) {
  362. if ($val == 'true' || $val == 'false') {
  363. $d[$key] = $val == 'true' ? true : false;
  364. } else {
  365. $val = explode(self::$arrayDelimiter, $val);
  366. if (count($val) > 1) {
  367. $d[$key] = $val;
  368. } else {
  369. $d[$key] =
  370. preg_replace('/\s+/msu', ' ',
  371. $d[$key]);
  372. }
  373. }
  374. }
  375. }
  376. $data = $d + $data;
  377. }
  378. }
  379. }
  380. return array($subject, $data);
  381. }
  382. private function formatThrows(array $value)
  383. {
  384. $code = 500;
  385. $exception = 'Exception';
  386. if (count($value) > 1) {
  387. $v1 = empty($value[0]) ? null : $value[0];
  388. $v2 = empty($value[1]) ? null : $value[1];
  389. if (is_numeric($v1)) {
  390. $code = $v1;
  391. $exception = $v2;
  392. array_shift($value);
  393. array_shift($value);
  394. } elseif (is_numeric($v2)) {
  395. $code = $v2;
  396. $exception = $v1;
  397. array_shift($value);
  398. array_shift($value);
  399. } else {
  400. $exception = $v1;
  401. array_shift($value);
  402. }
  403. } elseif (count($value) && isset($value[0]) && is_numeric($value[0])) {
  404. $code = $value[0];
  405. array_shift($value);
  406. }
  407. $message = implode(' ', $value);
  408. if (!isset(RestException::$codes[$code])) {
  409. $code = 500;
  410. } elseif (empty($message)) {
  411. $message = RestException::$codes[$code];
  412. }
  413. return compact('code', 'message', 'exception');
  414. }
  415. private function formatClass(array $value)
  416. {
  417. $param = array_shift($value);
  418. if (empty($param)) {
  419. $param = 'Unknown';
  420. }
  421. $value = implode(' ', $value);
  422. return array(
  423. ltrim($param, '\\'),
  424. array('description' => $value)
  425. );
  426. }
  427. private function formatAuthor(array $value)
  428. {
  429. $r = array();
  430. $email = end($value);
  431. if ($email[0] == '<') {
  432. $email = substr($email, 1, -1);
  433. array_pop($value);
  434. $r['email'] = $email;
  435. }
  436. $r['name'] = implode(' ', $value);
  437. return $r;
  438. }
  439. private function formatReturn(array $value)
  440. {
  441. $data = explode('|', array_shift($value));
  442. $r = array(
  443. 'type' => count($data) == 1 ? $data[0] : $data
  444. );
  445. $r['description'] = implode(' ', $value);
  446. return $r;
  447. }
  448. private function formatParam(array $value)
  449. {
  450. $r = array();
  451. $data = array_shift($value);
  452. if (empty($data)) {
  453. $r['type'] = 'mixed';
  454. } elseif ($data[0] == '$') {
  455. $r['name'] = substr($data, 1);
  456. $r['type'] = 'mixed';
  457. } else {
  458. $data = explode('|', $data);
  459. $r['type'] = count($data) == 1 ? $data[0] : $data;
  460. $data = array_shift($value);
  461. if (!empty($data) && $data[0] == '$') {
  462. $r['name'] = substr($data, 1);
  463. }
  464. }
  465. if (isset($r['type']) && is_string($r['type']) && Text::endsWith($r['type'], '[]')) {
  466. $r[static::$embeddedDataName]['type'] = substr($r['type'], 0, -2);
  467. $r['type'] = 'array';
  468. }
  469. if ($value) {
  470. $r['description'] = implode(' ', $value);
  471. }
  472. return $r;
  473. }
  474. private function formatVar(array $value)
  475. {
  476. $r = array();
  477. $data = array_shift($value);
  478. if (empty($data)) {
  479. $r['type'] = 'mixed';
  480. } elseif ($data[0] == '$') {
  481. $r['name'] = substr($data, 1);
  482. $r['type'] = 'mixed';
  483. } else {
  484. $data = explode('|', $data);
  485. $r['type'] = count($data) == 1 ? $data[0] : $data;
  486. }
  487. if (isset($r['type']) && Text::endsWith($r['type'], '[]')) {
  488. $r[static::$embeddedDataName]['type'] = substr($r['type'], 0, -2);
  489. $r['type'] = 'array';
  490. }
  491. if ($value) {
  492. $r['description'] = implode(' ', $value);
  493. }
  494. return $r;
  495. }
  496. }