Blueprint.php 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485
  1. <?php
  2. namespace Dingo\Blueprint;
  3. use ReflectionClass;
  4. use RuntimeException;
  5. use Illuminate\Support\Str;
  6. use Illuminate\Support\Collection;
  7. use Illuminate\Filesystem\Filesystem;
  8. use Doctrine\Common\Annotations\AnnotationRegistry;
  9. use Doctrine\Common\Annotations\SimpleAnnotationReader;
  10. class Blueprint
  11. {
  12. /**
  13. * Simple annotation reader instance.
  14. *
  15. * @var \Doctrine\Common\Annotations\SimpleAnnotationReader
  16. */
  17. protected $reader;
  18. /**
  19. * Filesytsem instance.
  20. *
  21. * @var \Illuminate\Filesystem\Filesystem
  22. */
  23. protected $files;
  24. /**
  25. * Include path for documentation files.
  26. *
  27. * @var string
  28. */
  29. protected $includePath;
  30. /**
  31. * Create a new generator instance.
  32. *
  33. * @param \Doctrine\Common\Annotations\SimpleAnnotationReader $reader
  34. * @param \Illuminate\Filesystem\Filesystem $files
  35. *
  36. * @return void
  37. */
  38. public function __construct(SimpleAnnotationReader $reader, Filesystem $files)
  39. {
  40. $this->reader = $reader;
  41. $this->files = $files;
  42. $this->registerAnnotationLoader();
  43. }
  44. /**
  45. * Register the annotation loader.
  46. *
  47. * @return void
  48. */
  49. protected function registerAnnotationLoader()
  50. {
  51. $this->reader->addNamespace('Dingo\\Blueprint\\Annotation');
  52. $this->reader->addNamespace('Dingo\\Blueprint\\Annotation\\Method');
  53. AnnotationRegistry::registerLoader(function ($class) {
  54. $path = __DIR__.'/'.str_replace(['Dingo\\Blueprint\\', '\\'], ['', DIRECTORY_SEPARATOR], $class).'.php';
  55. if (file_exists($path)) {
  56. require_once $path;
  57. return true;
  58. }
  59. });
  60. }
  61. /**
  62. * Generate documentation with the name, version and optional overview content.
  63. *
  64. * @param \Illuminate\Support\Collection $controllers
  65. * @param string $name
  66. * @param string $version
  67. * @param string $includePath
  68. * @param string $overviewFile
  69. *
  70. * @return bool
  71. */
  72. public function generate(Collection $controllers, $name, $version, $includePath = null, $overviewFile = null)
  73. {
  74. $this->includePath = $includePath;
  75. $resources = $controllers->map(function ($controller) use ($version) {
  76. $controller = $controller instanceof ReflectionClass ? $controller : new ReflectionClass($controller);
  77. $actions = new Collection;
  78. // Spin through all the methods on the controller and compare the version
  79. // annotation (if supplied) with the version given for the generation.
  80. // We'll also build up an array of actions on each resource.
  81. foreach ($controller->getMethods() as $method) {
  82. if ($versionAnnotation = $this->reader->getMethodAnnotation($method, Annotation\Versions::class)) {
  83. if (! in_array($version, $versionAnnotation->value)) {
  84. continue;
  85. }
  86. }
  87. if ($annotations = $this->reader->getMethodAnnotations($method)) {
  88. if (! $actions->contains($method)) {
  89. $actions->push(new Action($method, new Collection($annotations)));
  90. }
  91. }
  92. }
  93. $annotations = new Collection($this->reader->getClassAnnotations($controller));
  94. return new RestResource($controller->getName(), $controller, $annotations, $actions);
  95. });
  96. $contents = $this->generateContentsFromResources($resources, $name, $overviewFile);
  97. $this->includePath = null;
  98. return $contents;
  99. }
  100. /**
  101. * Generate the documentation contents from the resources collection.
  102. *
  103. * @param \Illuminate\Support\Collection $resources
  104. * @param string $name
  105. * @param string $overviewFile
  106. *
  107. * @return string
  108. */
  109. protected function generateContentsFromResources(Collection $resources, $name, $overviewFile = null)
  110. {
  111. $contents = '';
  112. $contents .= $this->getFormat();
  113. $contents .= $this->line(2);
  114. $contents .= sprintf('# %s', $name);
  115. $contents .= $this->line(2);
  116. $contents .= $this->getOverview($overviewFile);
  117. $resources->each(function ($resource) use (&$contents) {
  118. if ($resource->getActions()->isEmpty()) {
  119. return;
  120. }
  121. $contents .= $resource->getDefinition();
  122. if ($description = $resource->getDescription()) {
  123. $contents .= $this->line();
  124. $contents .= $description;
  125. }
  126. if (($parameters = $resource->getParameters()) && ! $parameters->isEmpty()) {
  127. $this->appendParameters($contents, $parameters);
  128. }
  129. $resource->getActions()->each(function ($action) use (&$contents, $resource) {
  130. $contents .= $this->line(2);
  131. $contents .= $action->getDefinition();
  132. if ($description = $action->getDescription()) {
  133. $contents .= $this->line();
  134. $contents .= $description;
  135. }
  136. if (($attributes = $action->getAttributes()) && ! $attributes->isEmpty()) {
  137. $this->appendAttributes($contents, $attributes);
  138. }
  139. if (($parameters = $action->getParameters()) && ! $parameters->isEmpty()) {
  140. $this->appendParameters($contents, $parameters);
  141. }
  142. if ($request = $action->getRequest()) {
  143. $this->appendRequest($contents, $request, $resource);
  144. }
  145. if ($response = $action->getResponse()) {
  146. $this->appendResponse($contents, $response, $resource);
  147. }
  148. if ($transaction = $action->getTransaction()) {
  149. foreach ($transaction->value as $value) {
  150. if ($value instanceof Annotation\Request) {
  151. $this->appendRequest($contents, $value, $resource);
  152. } elseif ($value instanceof Annotation\Response) {
  153. $this->appendResponse($contents, $value, $resource);
  154. } else {
  155. throw new RuntimeException('Unsupported annotation type given in transaction.');
  156. }
  157. }
  158. }
  159. });
  160. $contents .= $this->line(2);
  161. });
  162. return stripslashes(trim($contents));
  163. }
  164. /**
  165. * Append the attributes subsection to a resource or action.
  166. *
  167. * @param string $contents
  168. * @param \Illuminate\Support\Collection $attributes
  169. * @param int $indent
  170. *
  171. * @return void
  172. */
  173. protected function appendAttributes(&$contents, Collection $attributes, $indent = 0)
  174. {
  175. $this->appendSection($contents, 'Attributes', $indent);
  176. $attributes->each(function ($attribute) use (&$contents, $indent) {
  177. $contents .= $this->line();
  178. $contents .= $this->tab(1 + $indent);
  179. $contents .= sprintf('+ %s', $attribute->identifier);
  180. if ($attribute->sample) {
  181. $contents .= sprintf(': %s', $attribute->sample);
  182. }
  183. $contents .= sprintf(
  184. ' (%s, %s) - %s',
  185. $attribute->type,
  186. $attribute->required ? 'required' : 'optional',
  187. $attribute->description
  188. );
  189. });
  190. }
  191. /**
  192. * Append the parameters subsection to a resource or action.
  193. *
  194. * @param string $contents
  195. * @param \Illuminate\Support\Collection $parameters
  196. *
  197. * @return void
  198. */
  199. protected function appendParameters(&$contents, Collection $parameters)
  200. {
  201. $this->appendSection($contents, 'Parameters');
  202. $parameters->each(function ($parameter) use (&$contents) {
  203. $contents .= $this->line();
  204. $contents .= $this->tab();
  205. $contents .= sprintf(
  206. '+ %s:%s (%s, %s) - %s',
  207. $parameter->identifier,
  208. $parameter->example ? " `{$parameter->example}`" : '',
  209. $parameter->members ? sprintf('enum[%s]', $parameter->type) : $parameter->type,
  210. $parameter->required ? 'required' : 'optional',
  211. $parameter->description
  212. );
  213. if (isset($parameter->default)) {
  214. $this->appendSection($contents, sprintf('Default: %s', $parameter->default), 2, 1);
  215. }
  216. if (isset($parameter->members)) {
  217. $this->appendSection($contents, 'Members', 2, 1);
  218. foreach ($parameter->members as $member) {
  219. $this->appendSection($contents, sprintf('`%s` - %s', $member->identifier, $member->description), 3, 1);
  220. }
  221. }
  222. });
  223. }
  224. /**
  225. * Append a response subsection to an action.
  226. *
  227. * @param string $contents
  228. * @param \Dingo\Blueprint\Annotation\Response $response
  229. * @param \Dingo\Blueprint\RestResource $resource
  230. *
  231. * @return void
  232. */
  233. protected function appendResponse(&$contents, Annotation\Response $response, RestResource $resource)
  234. {
  235. $this->appendSection($contents, sprintf('Response %s', $response->statusCode));
  236. if (isset($response->contentType)) {
  237. $contents .= ' ('.$response->contentType.')';
  238. }
  239. if (! empty($response->headers) || $resource->hasResponseHeaders()) {
  240. $this->appendHeaders($contents, array_merge($resource->getResponseHeaders(), $response->headers));
  241. }
  242. if (isset($response->attributes)) {
  243. $this->appendAttributes($contents, collect($response->attributes), 1);
  244. }
  245. if (isset($response->body)) {
  246. $this->appendBody($contents, $this->prepareBody($response->body, $response->contentType));
  247. }
  248. }
  249. /**
  250. * Append a request subsection to an action.
  251. *
  252. * @param string $contents
  253. * @param \Dingo\Blueprint\Annotation\Request $request
  254. * @param \Dingo\Blueprint\RestResource $resource
  255. *
  256. * @return void
  257. */
  258. protected function appendRequest(&$contents, $request, RestResource $resource)
  259. {
  260. $this->appendSection($contents, 'Request');
  261. if (isset($request->identifier)) {
  262. $contents .= ' '.$request->identifier;
  263. }
  264. $contents .= ' ('.$request->contentType.')';
  265. if (! empty($request->headers) || $resource->hasRequestHeaders()) {
  266. $this->appendHeaders($contents, array_merge($resource->getRequestHeaders(), $request->headers));
  267. }
  268. if (isset($request->attributes)) {
  269. $this->appendAttributes($contents, collect($request->attributes), 1);
  270. }
  271. if (isset($request->body)) {
  272. $this->appendBody($contents, $this->prepareBody($request->body, $request->contentType));
  273. }
  274. }
  275. /**
  276. * Append a body subsection to an action.
  277. *
  278. * @param string $contents
  279. * @param string $body
  280. *
  281. * @return void
  282. */
  283. protected function appendBody(&$contents, $body)
  284. {
  285. $this->appendSection($contents, 'Body', 1, 1);
  286. $contents .= $this->line(2);
  287. $line = strtok($body, "\r\n");
  288. while ($line !== false) {
  289. $contents .= $this->tab(3).$line;
  290. $line = strtok("\r\n");
  291. if ($line !== false) {
  292. $contents .= $this->line();
  293. }
  294. }
  295. }
  296. /**
  297. * Append a headers subsection to an action.
  298. *
  299. * @param string $contents
  300. * @param array $headers
  301. *
  302. * @return void
  303. */
  304. protected function appendHeaders(&$contents, array $headers)
  305. {
  306. $this->appendSection($contents, 'Headers', 1, 1);
  307. $contents .= $this->line();
  308. foreach ($headers as $header => $value) {
  309. $contents .= $this->line().$this->tab(3).sprintf('%s: %s', $header, $value);
  310. }
  311. }
  312. /**
  313. * Append a subsection to an action.
  314. *
  315. * @param string $contents
  316. * @param string $name
  317. * @param int $indent
  318. * @param int $lines
  319. *
  320. * @return void
  321. */
  322. protected function appendSection(&$contents, $name, $indent = 0, $lines = 2)
  323. {
  324. $contents .= $this->line($lines);
  325. $contents .= $this->tab($indent);
  326. $contents .= '+ '.$name;
  327. }
  328. /**
  329. * Prepare a body.
  330. *
  331. * @param string $body
  332. * @param string $contentType
  333. *
  334. * @return string
  335. */
  336. protected function prepareBody($body, $contentType)
  337. {
  338. if (is_string($body) && Str::startsWith($body, ['json', 'file'])) {
  339. list($type, $path) = explode(':', $body);
  340. if (! Str::endsWith($path, '.json') && $type == 'json') {
  341. $path .= '.json';
  342. }
  343. $body = $this->files->get($this->includePath.'/'.$path);
  344. json_decode($body);
  345. if (json_last_error() == JSON_ERROR_NONE) {
  346. return $body;
  347. }
  348. }
  349. if (strpos($contentType, 'application/json') === 0) {
  350. return json_encode($body, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
  351. }
  352. return $body;
  353. }
  354. /**
  355. * Create a new line character.
  356. *
  357. * @param int $repeat
  358. *
  359. * @return string
  360. */
  361. protected function line($repeat = 1)
  362. {
  363. return str_repeat("\n", $repeat);
  364. }
  365. /**
  366. * Create a tab character.
  367. *
  368. * @param int $repeat
  369. *
  370. * @return string
  371. */
  372. protected function tab($repeat = 1)
  373. {
  374. return str_repeat(' ', $repeat);
  375. }
  376. /**
  377. * Get the API Blueprint format.
  378. *
  379. * @return string
  380. */
  381. protected function getFormat()
  382. {
  383. return 'FORMAT: 1A';
  384. }
  385. /**
  386. * Get the overview file content to append.
  387. *
  388. * @param null $file
  389. * @return null|string
  390. */
  391. protected function getOverview($file = null)
  392. {
  393. if (null !== $file) {
  394. if (!file_exists($file)) {
  395. throw new RuntimeException('Overview file could not be found.');
  396. }
  397. $content = file_get_contents($file);
  398. if ($content === false) {
  399. throw new RuntimeException('Failed to read overview file contents.');
  400. }
  401. return $content.$this->line(2);
  402. }
  403. return null;
  404. }
  405. }