api de gestion de ticket, basé sur php-crud-api. Le but est de décorrélé les outils de gestion des données, afin
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

OpenApiBuilder.php 17KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336
  1. <?php
  2. namespace Tqdev\PhpCrudApi\OpenApi;
  3. use Tqdev\PhpCrudApi\Column\ReflectionService;
  4. use Tqdev\PhpCrudApi\Middleware\Communication\VariableStore;
  5. use Tqdev\PhpCrudApi\OpenApi\OpenApiDefinition;
  6. class OpenApiBuilder
  7. {
  8. private $openapi;
  9. private $reflection;
  10. private $operations = [
  11. 'list' => 'get',
  12. 'create' => 'post',
  13. 'read' => 'get',
  14. 'update' => 'put',
  15. 'delete' => 'delete',
  16. 'increment' => 'patch',
  17. ];
  18. private $types = [
  19. 'integer' => ['type' => 'integer', 'format' => 'int32'],
  20. 'bigint' => ['type' => 'integer', 'format' => 'int64'],
  21. 'varchar' => ['type' => 'string'],
  22. 'clob' => ['type' => 'string'],
  23. 'varbinary' => ['type' => 'string', 'format' => 'byte'],
  24. 'blob' => ['type' => 'string', 'format' => 'byte'],
  25. 'decimal' => ['type' => 'string'],
  26. 'float' => ['type' => 'number', 'format' => 'float'],
  27. 'double' => ['type' => 'number', 'format' => 'double'],
  28. 'date' => ['type' => 'string', 'format' => 'date'],
  29. 'time' => ['type' => 'string', 'format' => 'date-time'],
  30. 'timestamp' => ['type' => 'string', 'format' => 'date-time'],
  31. 'geometry' => ['type' => 'string'],
  32. 'boolean' => ['type' => 'boolean'],
  33. ];
  34. public function __construct(ReflectionService $reflection, $base)
  35. {
  36. $this->reflection = $reflection;
  37. $this->openapi = new OpenApiDefinition($base);
  38. }
  39. private function getServerUrl(): string
  40. {
  41. $protocol = @$_SERVER['HTTP_X_FORWARDED_PROTO'] ?: @$_SERVER['REQUEST_SCHEME'] ?: ((isset($_SERVER["HTTPS"]) && $_SERVER["HTTPS"] == "on") ? "https" : "http");
  42. $port = @intval($_SERVER['HTTP_X_FORWARDED_PORT']) ?: @intval($_SERVER["SERVER_PORT"]) ?: (($protocol === 'https') ? 443 : 80);
  43. $host = @explode(":", $_SERVER['HTTP_HOST'])[0] ?: @$_SERVER['SERVER_NAME'] ?: @$_SERVER['SERVER_ADDR'];
  44. $port = ($protocol === 'https' && $port === 443) || ($protocol === 'http' && $port === 80) ? '' : ':' . $port;
  45. $path = @trim(substr($_SERVER['REQUEST_URI'], 0, strpos($_SERVER['REQUEST_URI'], '/openapi')), '/');
  46. return sprintf('%s://%s%s/%s', $protocol, $host, $port, $path);
  47. }
  48. private function getAllTableReferences(): array
  49. {
  50. $tableReferences = array();
  51. foreach ($this->reflection->getTableNames() as $tableName) {
  52. $table = $this->reflection->getTable($tableName);
  53. foreach ($table->getColumnNames() as $columnName) {
  54. $column = $table->getColumn($columnName);
  55. $referencedTableName = $column->getFk();
  56. if ($referencedTableName) {
  57. if (!isset($tableReferences[$referencedTableName])) {
  58. $tableReferences[$referencedTableName] = array();
  59. }
  60. $tableReferences[$referencedTableName][] = "$tableName.$columnName";
  61. }
  62. }
  63. }
  64. return $tableReferences;
  65. }
  66. public function build(): OpenApiDefinition
  67. {
  68. $this->openapi->set("openapi", "3.0.0");
  69. if (!$this->openapi->has("servers") && isset($_SERVER['REQUEST_URI'])) {
  70. $this->openapi->set("servers|0|url", $this->getServerUrl());
  71. }
  72. $tableNames = $this->reflection->getTableNames();
  73. foreach ($tableNames as $tableName) {
  74. $this->setPath($tableName);
  75. }
  76. $this->openapi->set("components|responses|pk_integer|description", "inserted primary key value (integer)");
  77. $this->openapi->set("components|responses|pk_integer|content|application/json|schema|type", "integer");
  78. $this->openapi->set("components|responses|pk_integer|content|application/json|schema|format", "int64");
  79. $this->openapi->set("components|responses|pk_string|description", "inserted primary key value (string)");
  80. $this->openapi->set("components|responses|pk_string|content|application/json|schema|type", "string");
  81. $this->openapi->set("components|responses|pk_string|content|application/json|schema|format", "uuid");
  82. $this->openapi->set("components|responses|rows_affected|description", "number of rows affected (integer)");
  83. $this->openapi->set("components|responses|rows_affected|content|application/json|schema|type", "integer");
  84. $this->openapi->set("components|responses|rows_affected|content|application/json|schema|format", "int64");
  85. $tableReferences = $this->getAllTableReferences();
  86. foreach ($tableNames as $tableName) {
  87. $references = isset($tableReferences[$tableName]) ? $tableReferences[$tableName] : array();
  88. $this->setComponentSchema($tableName, $references);
  89. $this->setComponentResponse($tableName);
  90. $this->setComponentRequestBody($tableName);
  91. }
  92. $this->setComponentParameters();
  93. foreach ($tableNames as $index => $tableName) {
  94. $this->setTag($index, $tableName);
  95. }
  96. return $this->openapi;
  97. }
  98. private function isOperationOnTableAllowed(string $operation, string $tableName): bool
  99. {
  100. $tableHandler = VariableStore::get('authorization.tableHandler');
  101. if (!$tableHandler) {
  102. return true;
  103. }
  104. return (bool) call_user_func($tableHandler, $operation, $tableName);
  105. }
  106. private function isOperationOnColumnAllowed(string $operation, string $tableName, string $columnName): bool
  107. {
  108. $columnHandler = VariableStore::get('authorization.columnHandler');
  109. if (!$columnHandler) {
  110. return true;
  111. }
  112. return (bool) call_user_func($columnHandler, $operation, $tableName, $columnName);
  113. }
  114. private function setPath(string $tableName) /*: void*/
  115. {
  116. $table = $this->reflection->getTable($tableName);
  117. $type = $table->getType();
  118. $pk = $table->getPk();
  119. $pkName = $pk ? $pk->getName() : '';
  120. foreach ($this->operations as $operation => $method) {
  121. if (!$pkName && $operation != 'list') {
  122. continue;
  123. }
  124. if ($type != 'table' && $operation != 'list') {
  125. continue;
  126. }
  127. if (!$this->isOperationOnTableAllowed($operation, $tableName)) {
  128. continue;
  129. }
  130. $parameters = [];
  131. if (in_array($operation, ['list', 'create'])) {
  132. $path = sprintf('/records/%s', $tableName);
  133. if ($operation == 'list') {
  134. $parameters = ['filter', 'include', 'exclude', 'order', 'size', 'page', 'join'];
  135. }
  136. } else {
  137. $path = sprintf('/records/%s/{%s}', $tableName, $pkName);
  138. if ($operation == 'read') {
  139. $parameters = ['pk', 'include', 'exclude', 'join'];
  140. } else {
  141. $parameters = ['pk'];
  142. }
  143. }
  144. foreach ($parameters as $p => $parameter) {
  145. $this->openapi->set("paths|$path|$method|parameters|$p|\$ref", "#/components/parameters/$parameter");
  146. }
  147. if (in_array($operation, ['create', 'update', 'increment'])) {
  148. $this->openapi->set("paths|$path|$method|requestBody|\$ref", "#/components/requestBodies/$operation-" . urlencode($tableName));
  149. }
  150. $this->openapi->set("paths|$path|$method|tags|0", "$tableName");
  151. $this->openapi->set("paths|$path|$method|description", "$operation $tableName");
  152. switch ($operation) {
  153. case 'list':
  154. $this->openapi->set("paths|$path|$method|responses|200|\$ref", "#/components/responses/$operation-" . urlencode($tableName));
  155. break;
  156. case 'create':
  157. if ($pk->getType() == 'integer') {
  158. $this->openapi->set("paths|$path|$method|responses|200|\$ref", "#/components/responses/pk_integer");
  159. } else {
  160. $this->openapi->set("paths|$path|$method|responses|200|\$ref", "#/components/responses/pk_string");
  161. }
  162. break;
  163. case 'read':
  164. $this->openapi->set("paths|$path|$method|responses|200|\$ref", "#/components/responses/$operation-" . urlencode($tableName));
  165. break;
  166. case 'update':
  167. case 'delete':
  168. case 'increment':
  169. $this->openapi->set("paths|$path|$method|responses|200|\$ref", "#/components/responses/rows_affected");
  170. break;
  171. }
  172. }
  173. }
  174. private function setComponentSchema(string $tableName, array $references) /*: void*/
  175. {
  176. $table = $this->reflection->getTable($tableName);
  177. $type = $table->getType();
  178. $pk = $table->getPk();
  179. $pkName = $pk ? $pk->getName() : '';
  180. foreach ($this->operations as $operation => $method) {
  181. if (!$pkName && $operation != 'list') {
  182. continue;
  183. }
  184. if ($type != 'table' && $operation != 'list') {
  185. continue;
  186. }
  187. if ($operation == 'delete') {
  188. continue;
  189. }
  190. if (!$this->isOperationOnTableAllowed($operation, $tableName)) {
  191. continue;
  192. }
  193. if ($operation == 'list') {
  194. $this->openapi->set("components|schemas|$operation-$tableName|type", "object");
  195. $this->openapi->set("components|schemas|$operation-$tableName|properties|results|type", "integer");
  196. $this->openapi->set("components|schemas|$operation-$tableName|properties|results|format", "int64");
  197. $this->openapi->set("components|schemas|$operation-$tableName|properties|records|type", "array");
  198. $prefix = "components|schemas|$operation-$tableName|properties|records|items";
  199. } else {
  200. $prefix = "components|schemas|$operation-$tableName";
  201. }
  202. $this->openapi->set("$prefix|type", "object");
  203. foreach ($table->getColumnNames() as $columnName) {
  204. if (!$this->isOperationOnColumnAllowed($operation, $tableName, $columnName)) {
  205. continue;
  206. }
  207. $column = $table->getColumn($columnName);
  208. $properties = $this->types[$column->getType()];
  209. foreach ($properties as $key => $value) {
  210. $this->openapi->set("$prefix|properties|$columnName|$key", $value);
  211. }
  212. if ($column->getPk()) {
  213. $this->openapi->set("$prefix|properties|$columnName|x-primary-key", true);
  214. $this->openapi->set("$prefix|properties|$columnName|x-referenced", $references);
  215. }
  216. $fk = $column->getFk();
  217. if ($fk) {
  218. $this->openapi->set("$prefix|properties|$columnName|x-references", $fk);
  219. }
  220. }
  221. }
  222. }
  223. private function setComponentResponse(string $tableName) /*: void*/
  224. {
  225. $table = $this->reflection->getTable($tableName);
  226. $type = $table->getType();
  227. $pk = $table->getPk();
  228. $pkName = $pk ? $pk->getName() : '';
  229. foreach (['list', 'read'] as $operation) {
  230. if (!$pkName && $operation != 'list') {
  231. continue;
  232. }
  233. if ($type != 'table' && $operation != 'list') {
  234. continue;
  235. }
  236. if (!$this->isOperationOnTableAllowed($operation, $tableName)) {
  237. continue;
  238. }
  239. if ($operation == 'list') {
  240. $this->openapi->set("components|responses|$operation-$tableName|description", "list of $tableName records");
  241. } else {
  242. $this->openapi->set("components|responses|$operation-$tableName|description", "single $tableName record");
  243. }
  244. $this->openapi->set("components|responses|$operation-$tableName|content|application/json|schema|\$ref", "#/components/schemas/$operation-" . urlencode($tableName));
  245. }
  246. }
  247. private function setComponentRequestBody(string $tableName) /*: void*/
  248. {
  249. $table = $this->reflection->getTable($tableName);
  250. $type = $table->getType();
  251. $pk = $table->getPk();
  252. $pkName = $pk ? $pk->getName() : '';
  253. if ($pkName && $type == 'table') {
  254. foreach (['create', 'update', 'increment'] as $operation) {
  255. if (!$this->isOperationOnTableAllowed($operation, $tableName)) {
  256. continue;
  257. }
  258. $this->openapi->set("components|requestBodies|$operation-$tableName|description", "single $tableName record");
  259. $this->openapi->set("components|requestBodies|$operation-$tableName|content|application/json|schema|\$ref", "#/components/schemas/$operation-" . urlencode($tableName));
  260. }
  261. }
  262. }
  263. private function setComponentParameters() /*: void*/
  264. {
  265. $this->openapi->set("components|parameters|pk|name", "id");
  266. $this->openapi->set("components|parameters|pk|in", "path");
  267. $this->openapi->set("components|parameters|pk|schema|type", "string");
  268. $this->openapi->set("components|parameters|pk|description", "primary key value");
  269. $this->openapi->set("components|parameters|pk|required", true);
  270. $this->openapi->set("components|parameters|filter|name", "filter");
  271. $this->openapi->set("components|parameters|filter|in", "query");
  272. $this->openapi->set("components|parameters|filter|schema|type", "array");
  273. $this->openapi->set("components|parameters|filter|schema|items|type", "string");
  274. $this->openapi->set("components|parameters|filter|description", "Filters to be applied. Each filter consists of a column, an operator and a value (comma separated). Example: id,eq,1");
  275. $this->openapi->set("components|parameters|filter|required", false);
  276. $this->openapi->set("components|parameters|include|name", "include");
  277. $this->openapi->set("components|parameters|include|in", "query");
  278. $this->openapi->set("components|parameters|include|schema|type", "string");
  279. $this->openapi->set("components|parameters|include|description", "Columns you want to include in the output (comma separated). Example: posts.*,categories.name");
  280. $this->openapi->set("components|parameters|include|required", false);
  281. $this->openapi->set("components|parameters|exclude|name", "exclude");
  282. $this->openapi->set("components|parameters|exclude|in", "query");
  283. $this->openapi->set("components|parameters|exclude|schema|type", "string");
  284. $this->openapi->set("components|parameters|exclude|description", "Columns you want to exclude from the output (comma separated). Example: posts.content");
  285. $this->openapi->set("components|parameters|exclude|required", false);
  286. $this->openapi->set("components|parameters|order|name", "order");
  287. $this->openapi->set("components|parameters|order|in", "query");
  288. $this->openapi->set("components|parameters|order|schema|type", "array");
  289. $this->openapi->set("components|parameters|order|schema|items|type", "string");
  290. $this->openapi->set("components|parameters|order|description", "Column you want to sort on and the sort direction (comma separated). Example: id,desc");
  291. $this->openapi->set("components|parameters|order|required", false);
  292. $this->openapi->set("components|parameters|size|name", "size");
  293. $this->openapi->set("components|parameters|size|in", "query");
  294. $this->openapi->set("components|parameters|size|schema|type", "string");
  295. $this->openapi->set("components|parameters|size|description", "Maximum number of results (for top lists). Example: 10");
  296. $this->openapi->set("components|parameters|size|required", false);
  297. $this->openapi->set("components|parameters|page|name", "page");
  298. $this->openapi->set("components|parameters|page|in", "query");
  299. $this->openapi->set("components|parameters|page|schema|type", "string");
  300. $this->openapi->set("components|parameters|page|description", "Page number and page size (comma separated). Example: 1,10");
  301. $this->openapi->set("components|parameters|page|required", false);
  302. $this->openapi->set("components|parameters|join|name", "join");
  303. $this->openapi->set("components|parameters|join|in", "query");
  304. $this->openapi->set("components|parameters|join|schema|type", "array");
  305. $this->openapi->set("components|parameters|join|schema|items|type", "string");
  306. $this->openapi->set("components|parameters|join|description", "Paths (comma separated) to related entities that you want to include. Example: comments,users");
  307. $this->openapi->set("components|parameters|join|required", false);
  308. }
  309. private function setTag(int $index, string $tableName) /*: void*/
  310. {
  311. $this->openapi->set("tags|$index|name", "$tableName");
  312. $this->openapi->set("tags|$index|description", "$tableName operations");
  313. }
  314. }