AbstractGenerator.php 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497
  1. <?php
  2. namespace Mpociot\ApiDoc\Generators;
  3. use Faker\Factory;
  4. use Illuminate\Foundation\Http\FormRequest;
  5. use Illuminate\Support\Arr;
  6. use Illuminate\Support\Facades\Validator;
  7. use Illuminate\Support\Str;
  8. use Mpociot\ApiDoc\Parsers\RuleDescriptionParser as Description;
  9. use ReflectionClass;
  10. use Mpociot\Reflection\DocBlock;
  11. abstract class AbstractGenerator
  12. {
  13. /**
  14. * @param $route
  15. *
  16. * @return mixed
  17. */
  18. abstract protected function getUri($route);
  19. /**
  20. * @param \Illuminate\Routing\Route $route
  21. * @param array $bindings
  22. * @param bool $withResponse
  23. *
  24. * @return array
  25. */
  26. abstract public function processRoute($route, $bindings = [], $withResponse = true);
  27. /**
  28. * @param array $routeData
  29. * @param array $routeAction
  30. * @param array $bindings
  31. *
  32. * @return mixed
  33. */
  34. protected function getParameters($routeData, $routeAction, $bindings)
  35. {
  36. $validator = Validator::make([], $this->getRouteRules($routeAction['uses'], $bindings));
  37. foreach ($validator->getRules() as $attribute => $rules) {
  38. $attributeData = [
  39. 'required' => false,
  40. 'type' => null,
  41. 'default' => '',
  42. 'value' => '',
  43. 'description' => [],
  44. ];
  45. foreach ($rules as $ruleName => $rule) {
  46. $this->parseRule($rule, $attribute, $attributeData, $routeData['id']);
  47. }
  48. $routeData['parameters'][$attribute] = $attributeData;
  49. }
  50. return $routeData;
  51. }
  52. /**
  53. * @param $route
  54. * @param $bindings
  55. * @param $headers
  56. *
  57. * @return \Illuminate\Http\Response
  58. */
  59. protected function getRouteResponse($route, $bindings, $headers = [])
  60. {
  61. $uri = $this->addRouteModelBindings($route, $bindings);
  62. $methods = $route->getMethods();
  63. // Split headers into key - value pairs
  64. $headers = collect($headers)->map(function($value) {
  65. $split = explode(':', $value);
  66. return [trim($split[0]) => trim($split[1])];
  67. })->collapse()->toArray();
  68. return $this->callRoute(array_shift($methods), $uri, [], [], [], $headers);
  69. }
  70. /**
  71. * @param $route
  72. * @param array $bindings
  73. *
  74. * @return mixed
  75. */
  76. protected function addRouteModelBindings($route, $bindings)
  77. {
  78. $uri = $this->getUri($route);
  79. foreach ($bindings as $model => $id) {
  80. $uri = str_replace('{'.$model.'}', $id, $uri);
  81. }
  82. return $uri;
  83. }
  84. /**
  85. * @param \Illuminate\Routing\Route $route
  86. *
  87. * @return string
  88. */
  89. protected function getRouteDescription($route)
  90. {
  91. list($class, $method) = explode('@', $route);
  92. $reflection = new ReflectionClass($class);
  93. $reflectionMethod = $reflection->getMethod($method);
  94. $comment = $reflectionMethod->getDocComment();
  95. $phpdoc = new DocBlock($comment);
  96. return [
  97. 'short' => $phpdoc->getShortDescription(),
  98. 'long' => $phpdoc->getLongDescription()->getContents(),
  99. ];
  100. }
  101. /**
  102. * @param string $route
  103. *
  104. * @return string
  105. */
  106. protected function getRouteGroup($route)
  107. {
  108. list($class, $method) = explode('@', $route);
  109. $reflection = new ReflectionClass($class);
  110. $comment = $reflection->getDocComment();
  111. if ($comment) {
  112. $phpdoc = new DocBlock($comment);
  113. foreach ($phpdoc->getTags() as $tag) {
  114. if ($tag->getName() === 'resource') {
  115. return $tag->getContent();
  116. }
  117. }
  118. }
  119. return 'general';
  120. }
  121. /**
  122. * @param $route
  123. * @param array $bindings
  124. *
  125. * @return array
  126. */
  127. protected function getRouteRules($route, $bindings)
  128. {
  129. list($class, $method) = explode('@', $route);
  130. $reflection = new ReflectionClass($class);
  131. $reflectionMethod = $reflection->getMethod($method);
  132. foreach ($reflectionMethod->getParameters() as $parameter) {
  133. $parameterType = $parameter->getClass();
  134. if (! is_null($parameterType) && class_exists($parameterType->name)) {
  135. $className = $parameterType->name;
  136. if (is_subclass_of($className, FormRequest::class)) {
  137. $parameterReflection = new $className;
  138. // Add route parameter bindings
  139. $parameterReflection->query->add($bindings);
  140. $parameterReflection->request->add($bindings);
  141. if (method_exists($parameterReflection, 'validator')) {
  142. return $parameterReflection->validator()->getRules();
  143. } else {
  144. return $parameterReflection->rules();
  145. }
  146. }
  147. }
  148. }
  149. return [];
  150. }
  151. /**
  152. * @param array $arr
  153. * @param string $first
  154. * @param string $last
  155. *
  156. * @return string
  157. */
  158. protected function fancyImplode($arr, $first, $last)
  159. {
  160. $arr = array_map(function ($value) {
  161. return '`'.$value.'`';
  162. }, $arr);
  163. array_push($arr, implode($last, array_splice($arr, -2)));
  164. return implode($first, $arr);
  165. }
  166. protected function splitValuePairs($parameters, $first = 'is ', $last = 'or ') {
  167. $attribute = '';
  168. collect($parameters)->map(function($item, $key) use (&$attribute, $first, $last) {
  169. $attribute .= '`'.$item.'` ';
  170. if (($key+1) % 2 === 0) {
  171. $attribute .= $last;
  172. } else {
  173. $attribute .= $first;
  174. }
  175. });
  176. $attribute = rtrim($attribute, $last);
  177. return $attribute;
  178. }
  179. /**
  180. * @param string $rule
  181. * @param string $ruleName
  182. * @param array $attributeData
  183. * @param int $seed
  184. *
  185. * @return void
  186. */
  187. protected function parseRule($rule, $ruleName, &$attributeData, $seed)
  188. {
  189. $faker = Factory::create();
  190. $faker->seed(crc32($seed));
  191. $parsedRule = $this->parseStringRule($rule);
  192. $parsedRule[0] = $this->normalizeRule($parsedRule[0]);
  193. list($rule, $parameters) = $parsedRule;
  194. switch ($rule) {
  195. case 'required':
  196. $attributeData['required'] = true;
  197. break;
  198. case 'accepted':
  199. $attributeData['required'] = true;
  200. $attributeData['type'] = 'boolean';
  201. $attributeData['value'] = true;
  202. break;
  203. case 'after':
  204. $attributeData['type'] = 'date';
  205. $attributeData['description'][] = Description::parse($rule)->with(date(DATE_RFC850, strtotime($parameters[0])))->getDescription();
  206. $attributeData['value'] = date(DATE_RFC850, strtotime('+1 day', strtotime($parameters[0])));
  207. break;
  208. case 'alpha':
  209. $attributeData['description'][] = Description::parse($rule)->getDescription();
  210. $attributeData['value'] = $faker->word;
  211. break;
  212. case 'alpha_dash':
  213. $attributeData['description'][] = Description::parse($rule)->getDescription();
  214. break;
  215. case 'alpha_num':
  216. $attributeData['description'][] = Description::parse($rule)->getDescription();
  217. break;
  218. case 'in':
  219. $attributeData['description'][] = Description::parse($rule)->with($this->fancyImplode($parameters, ', ', ' or '))->getDescription();
  220. $attributeData['value'] = $faker->randomElement($parameters);
  221. break;
  222. case 'not_in':
  223. $attributeData['description'][] = Description::parse($rule)->with($this->fancyImplode($parameters, ', ', ' or '))->getDescription();
  224. $attributeData['value'] = $faker->word;
  225. break;
  226. case 'min':
  227. $attributeData['description'][] = Description::parse($rule)->with($parameters)->getDescription();
  228. if (Arr::get($attributeData, 'type') === 'numeric' || Arr::get($attributeData, 'type') === 'integer') {
  229. $attributeData['value'] = $faker->numberBetween($parameters[0]);
  230. }
  231. break;
  232. case 'max':
  233. $attributeData['description'][] = Description::parse($rule)->with($parameters)->getDescription();
  234. if (Arr::get($attributeData, 'type') === 'numeric' || Arr::get($attributeData, 'type') === 'integer') {
  235. $attributeData['value'] = $faker->numberBetween(0, $parameters[0]);
  236. }
  237. break;
  238. case 'between':
  239. if (! isset($attributeData['type'])) {
  240. $attributeData['type'] = 'numeric';
  241. }
  242. $attributeData['description'][] = Description::parse($rule)->with($parameters)->getDescription();
  243. $attributeData['value'] = $faker->numberBetween($parameters[0], $parameters[1]);
  244. break;
  245. case 'before':
  246. $attributeData['type'] = 'date';
  247. $attributeData['description'][] = Description::parse($rule)->with(date(DATE_RFC850, strtotime($parameters[0])))->getDescription();
  248. $attributeData['value'] = date(DATE_RFC850, strtotime('-1 day', strtotime($parameters[0])));
  249. break;
  250. case 'date_format':
  251. $attributeData['type'] = 'date';
  252. $attributeData['description'][] = Description::parse($rule)->with($parameters)->getDescription();
  253. $attributeData['value'] = date($parameters[0]);
  254. break;
  255. case 'different':
  256. $attributeData['description'][] = Description::parse($rule)->with($parameters)->getDescription();
  257. break;
  258. case 'digits':
  259. $attributeData['type'] = 'numeric';
  260. $attributeData['description'][] = Description::parse($rule)->with($parameters)->getDescription();
  261. $attributeData['value'] = $faker->randomNumber($parameters[0], true);
  262. break;
  263. case 'digits_between':
  264. $attributeData['type'] = 'numeric';
  265. $attributeData['description'][] = Description::parse($rule)->with($parameters)->getDescription();
  266. break;
  267. case 'file':
  268. $attributeData['type'] = 'file';
  269. $attributeData['description'][] = Description::parse($rule)->getDescription();
  270. break;
  271. case 'image':
  272. $attributeData['type'] = 'image';
  273. $attributeData['description'][] = Description::parse($rule)->getDescription();
  274. break;
  275. case 'json':
  276. $attributeData['type'] = 'string';
  277. $attributeData['description'][] = Description::parse($rule)->getDescription();
  278. $attributeData['value'] = json_encode(['foo', 'bar', 'baz']);
  279. break;
  280. case 'mimetypes':
  281. case 'mimes':
  282. $attributeData['description'][] = Description::parse($rule)->with($this->fancyImplode($parameters, ', ', ' or '))->getDescription();
  283. break;
  284. case 'required_if':
  285. $attributeData['description'][] = Description::parse($rule)->with($this->splitValuePairs($parameters))->getDescription();
  286. break;
  287. case 'required_unless':
  288. $attributeData['description'][] = Description::parse($rule)->with($this->splitValuePairs($parameters))->getDescription();
  289. break;
  290. case 'required_with':
  291. $attributeData['description'][] = Description::parse($rule)->with($this->fancyImplode($parameters, ', ', ' or '))->getDescription();
  292. break;
  293. case 'required_with_all':
  294. $attributeData['description'][] = Description::parse($rule)->with($this->fancyImplode($parameters, ', ', ' and '))->getDescription();
  295. break;
  296. case 'required_without':
  297. $attributeData['description'][] = Description::parse($rule)->with($this->fancyImplode($parameters, ', ', ' or '))->getDescription();
  298. break;
  299. case 'required_without_all':
  300. $attributeData['description'][] = Description::parse($rule)->with($this->fancyImplode($parameters, ', ', ' and '))->getDescription();
  301. break;
  302. case 'same':
  303. $attributeData['description'][] = Description::parse($rule)->with($parameters)->getDescription();
  304. break;
  305. case 'size':
  306. $attributeData['description'][] = Description::parse($rule)->with($parameters)->getDescription();
  307. break;
  308. case 'timezone':
  309. $attributeData['description'][] = Description::parse($rule)->getDescription();
  310. $attributeData['value'] = $faker->timezone;
  311. break;
  312. case 'exists':
  313. $fieldName = isset($parameters[1]) ? $parameters[1] : $ruleName;
  314. $attributeData['description'][] = Description::parse($rule)->with([Str::singular($parameters[0]), $fieldName])->getDescription();
  315. break;
  316. case 'active_url':
  317. $attributeData['type'] = 'url';
  318. $attributeData['value'] = $faker->url;
  319. break;
  320. case 'regex':
  321. $attributeData['type'] = 'string';
  322. $attributeData['description'][] = Description::parse($rule)->with($parameters)->getDescription();
  323. break;
  324. case 'boolean':
  325. $attributeData['value'] = true;
  326. $attributeData['type'] = $rule;
  327. break;
  328. case 'array':
  329. $attributeData['value'] = $faker->word;
  330. $attributeData['type'] = $rule;
  331. break;
  332. case 'date':
  333. $attributeData['value'] = $faker->date();
  334. $attributeData['type'] = $rule;
  335. break;
  336. case 'email':
  337. $attributeData['value'] = $faker->safeEmail;
  338. $attributeData['type'] = $rule;
  339. break;
  340. case 'string':
  341. $attributeData['value'] = $faker->word;
  342. $attributeData['type'] = $rule;
  343. break;
  344. case 'integer':
  345. $attributeData['value'] = $faker->randomNumber();
  346. $attributeData['type'] = $rule;
  347. break;
  348. case 'numeric':
  349. $attributeData['value'] = $faker->randomNumber();
  350. $attributeData['type'] = $rule;
  351. break;
  352. case 'url':
  353. $attributeData['value'] = $faker->url;
  354. $attributeData['type'] = $rule;
  355. break;
  356. case 'ip':
  357. $attributeData['value'] = $faker->ipv4;
  358. $attributeData['type'] = $rule;
  359. break;
  360. }
  361. if ($attributeData['value'] === '') {
  362. $attributeData['value'] = $faker->word;
  363. }
  364. if (is_null($attributeData['type'])) {
  365. $attributeData['type'] = 'string';
  366. }
  367. }
  368. /**
  369. * Call the given URI and return the Response.
  370. *
  371. * @param string $method
  372. * @param string $uri
  373. * @param array $parameters
  374. * @param array $cookies
  375. * @param array $files
  376. * @param array $server
  377. * @param string $content
  378. *
  379. * @return \Illuminate\Http\Response
  380. */
  381. abstract public function callRoute($method, $uri, $parameters = [], $cookies = [], $files = [], $server = [], $content = null);
  382. /**
  383. * Transform headers array to array of $_SERVER vars with HTTP_* format.
  384. *
  385. * @param array $headers
  386. *
  387. * @return array
  388. */
  389. protected function transformHeadersToServerVars(array $headers)
  390. {
  391. $server = [];
  392. $prefix = 'HTTP_';
  393. foreach ($headers as $name => $value) {
  394. $name = strtr(strtoupper($name), '-', '_');
  395. if (! Str::startsWith($name, $prefix) && $name !== 'CONTENT_TYPE') {
  396. $name = $prefix.$name;
  397. }
  398. $server[$name] = $value;
  399. }
  400. return $server;
  401. }
  402. /**
  403. * Parse a string based rule.
  404. *
  405. * @param string $rules
  406. *
  407. * @return array
  408. */
  409. protected function parseStringRule($rules)
  410. {
  411. $parameters = [];
  412. // The format for specifying validation rules and parameters follows an
  413. // easy {rule}:{parameters} formatting convention. For instance the
  414. // rule "Max:3" states that the value may only be three letters.
  415. if (strpos($rules, ':') !== false) {
  416. list($rules, $parameter) = explode(':', $rules, 2);
  417. $parameters = $this->parseParameters($rules, $parameter);
  418. }
  419. return [strtolower(trim($rules)), $parameters];
  420. }
  421. /**
  422. * Parse a parameter list.
  423. *
  424. * @param string $rule
  425. * @param string $parameter
  426. *
  427. * @return array
  428. */
  429. protected function parseParameters($rule, $parameter)
  430. {
  431. if (strtolower($rule) === 'regex') {
  432. return [$parameter];
  433. }
  434. return str_getcsv($parameter);
  435. }
  436. /**
  437. * Normalizes a rule so that we can accept short types.
  438. *
  439. * @param string $rule
  440. *
  441. * @return string
  442. */
  443. protected function normalizeRule($rule)
  444. {
  445. switch ($rule) {
  446. case 'int':
  447. return 'integer';
  448. case 'bool':
  449. return 'boolean';
  450. default:
  451. return $rule;
  452. }
  453. }
  454. }