Nelze vybrat více než 25 témat Téma musí začínat písmenem nebo číslem, může obsahovat pomlčky („-“) a může být dlouhé až 35 znaků.

514 řádky
13KB

  1. <?php
  2. namespace App\Kintone;
  3. use App\Exceptions\AppCommonException;
  4. use App\Exceptions\ConfigException;
  5. use App\Exceptions\ExclusiveException;
  6. use App\Files\TmpFile;
  7. use App\Kintone\Models\KintoneModel;
  8. use Exception;
  9. use Illuminate\Database\Eloquent\ModelNotFoundException;
  10. use Illuminate\Http\UploadedFile;
  11. use Illuminate\Support\Collection;
  12. use Illuminate\Support\Facades\Http;
  13. use Log;
  14. /**
  15. * @template TValue of KintoneModel
  16. */
  17. class KintoneAccess
  18. {
  19. private string $appName;
  20. private ?string $modelName;
  21. private string $host;
  22. private string $apiToken = "";
  23. private int $appId;
  24. private const DEFAULT_FIELDS = [
  25. "作成日時",
  26. "更新日時",
  27. '$id',
  28. '$revision',
  29. ];
  30. private array $fields = [];
  31. private string|null $cursor = null;
  32. private int $cursorDataCount = 0;
  33. private bool $hasNext = false;
  34. private const URL_RECORD = "/k/v1/record.json";
  35. private const URL_RECORDS = "/k/v1/records.json";
  36. private const URL_CURSOL = "/k/v1/records/cursor.json";
  37. private const URL_FILE = "/k/v1/file.json";
  38. private const URL_APP_FORM_FIELDS = "/k/v1/app/form/fields.json";
  39. public function __construct(string $appName, ?string $modelName)
  40. {
  41. $this->appName = $appName;
  42. $this->modelName = $modelName;
  43. $key = "kintone.host";
  44. $host = config($key);
  45. if (!$host) {
  46. throw new ConfigException("kintone.host", $host);
  47. }
  48. $key = "kintone.applications." . $appName . ".apiToken";
  49. $apiToken = config($key);
  50. if (!$apiToken) {
  51. throw new ConfigException($key, $apiToken);
  52. }
  53. $key = "kintone.applications." . $appName . ".appId";
  54. $appId = config($key);
  55. if (!$appId) {
  56. throw new ConfigException($key, $appId);
  57. }
  58. $this->addAppToken($appName);
  59. $this->host = $host;
  60. $this->appId = $appId;
  61. }
  62. public function addAppToken(string $appName): static
  63. {
  64. $key = "kintone.applications." . $appName . ".apiToken";
  65. $apiToken = config($key);
  66. if (!$apiToken) {
  67. throw new ConfigException($key, $apiToken);
  68. }
  69. $this->apiToken = $this->apiToken ? $this->apiToken . "," . $apiToken : $apiToken;
  70. return $this;
  71. }
  72. public function setFields(array $fields): static
  73. {
  74. $this->fields = [
  75. ...$fields,
  76. ...self::DEFAULT_FIELDS,
  77. ];
  78. return $this;
  79. }
  80. public function __destruct()
  81. {
  82. $this->deleteCursor();
  83. }
  84. /**
  85. * @param integer $id
  86. * @return TValue
  87. */
  88. public function find(int $id)
  89. {
  90. $response = Http::withHeaders([
  91. "X-Cybozu-API-Token" => $this->apiToken,
  92. ])->get($this->getRecordUrl(), [
  93. "app" => $this->appId,
  94. "id" => $id,
  95. ]);
  96. if ($response->failed()) {
  97. $e = $response->toException();
  98. if ($e instanceof Exception) {
  99. Log::error($e->getMessage());
  100. throw $e;
  101. }
  102. }
  103. $result = $this->modelName ? new $this->modelName() : new $this->appName();
  104. $result->setDataFromRecordResponse($response['record']);
  105. return $result;
  106. }
  107. /**
  108. * @param KintoneRecordQuery|null|null $query
  109. * @return Collection<string,TValue>
  110. */
  111. public function some(KintoneRecordQuery|null $query = null)
  112. {
  113. $data = [
  114. "app" => $this->appId,
  115. 'fields' => $this->fields,
  116. ];
  117. if ($query !== null) {
  118. $data["query"] = $query->toQuery();
  119. }
  120. $response = Http::withHeaders([
  121. "X-Cybozu-API-Token" => $this->apiToken,
  122. ])->get($this->getRecordsUrl(), $data);
  123. if ($response->failed()) {
  124. $e = $response->toException();
  125. if ($e instanceof Exception) {
  126. Log::error($e->getMessage());
  127. throw $e;
  128. }
  129. }
  130. $ret = collect();
  131. foreach ($response["records"] as $data) {
  132. /**
  133. * @var KintoneModel $model
  134. */
  135. $model = $this->modelName ? new $this->modelName() : new $this->appName();
  136. $model->setDataFromRecordResponse($data);
  137. $ret->put($model->getRecordId(), $model);
  138. }
  139. return $ret;
  140. }
  141. /**
  142. * @param KintoneRecordQuery|null|null $query
  143. * @return TValue
  144. */
  145. public function first(KintoneRecordQuery|null $query = null)
  146. {
  147. $list = $this->some($query);
  148. if ($list->count() !== 1) {
  149. throw new ModelNotFoundException(sprintf("モデル取得数エラー %s count:%d queey:<%s>", $this->appName, $list->count(), $query->toQuery()));
  150. }
  151. return $list->first();
  152. }
  153. /**
  154. * @param TValue $model
  155. * @return void
  156. */
  157. public function update(KintoneModel &$model)
  158. {
  159. $sendData = [
  160. "app" => $this->appId,
  161. "id" => $model->getRecordId(),
  162. "record" => $model->getApiLayout(),
  163. "revision" => $model->getRevision(),
  164. ];
  165. $response = Http::withHeaders([
  166. "X-Cybozu-API-Token" => $this->apiToken,
  167. ])->put($this->getRecordUrl(), $sendData);
  168. if ($response->failed()) {
  169. $e = $response->toException();
  170. if ($e instanceof Exception) {
  171. $context = [
  172. "action" => "KINTONE_UPDATE",
  173. "appName" => $this->appName,
  174. "appId" => $this->appId,
  175. "recordId" => $model->getRecordId(),
  176. "sendRevision" => $model->getRevision(),
  177. "sendRecord" => $sendData["record"],
  178. "status" => $response->status(),
  179. "responseBody" => $response->body(),
  180. ];
  181. if ($response->status() === 409) {
  182. try {
  183. $current = $this->find((int)$model->getRecordId());
  184. $context["currentRevision"] = $current->getRevision();
  185. } catch (Exception $findException) {
  186. $context["currentRevisionError"] = $findException->getMessage();
  187. }
  188. Log::error(sprintf("Kintone排他エラー:%s", $e->getMessage()), $context);
  189. throw new ExclusiveException($e->getMessage(), (int)$e->getCode(), $e);
  190. }
  191. Log::error($e->getMessage(), $context);
  192. throw $e;
  193. }
  194. }
  195. $model->clean($response["revision"]);
  196. return $response;
  197. }
  198. /**
  199. * @param TValue $model
  200. * @return void
  201. */
  202. public function create(KintoneModel &$model)
  203. {
  204. $sendData = [
  205. "app" => $this->appId,
  206. "record" => $model->getApiLayout(),
  207. ];
  208. $response = Http::withHeaders([
  209. "X-Cybozu-API-Token" => $this->apiToken,
  210. ])->post($this->getRecordUrl(), $sendData);
  211. if ($response->failed()) {
  212. $e = $response->toException();
  213. if ($e instanceof Exception) {
  214. Log::error($e->getMessage(), [
  215. "action" => "KINTONE_CREATE",
  216. "appName" => $this->appName,
  217. "appId" => $this->appId,
  218. "sendRecord" => $sendData["record"],
  219. "status" => $response->status(),
  220. "responseBody" => $response->body(),
  221. ]);
  222. throw $e;
  223. }
  224. }
  225. $model->clean($response["revision"]);
  226. $model->setRecordId($response["id"]);
  227. return $response;
  228. }
  229. public function createCursor(KintoneRecordQuery|null $query = null)
  230. {
  231. $data = [
  232. "app" => $this->appId,
  233. 'fields' => $this->fields,
  234. ];
  235. if ($query !== null) {
  236. $data["query"] = $query->toQuery();
  237. }
  238. $response = Http::withHeaders([
  239. "X-Cybozu-API-Token" => $this->apiToken,
  240. ])->post($this->getCursorUrl(), $data);
  241. if ($response->failed()) {
  242. $e = $response->toException();
  243. if ($e instanceof Exception) {
  244. Log::error($e->getMessage(), ['data' => $data]);
  245. Log::error($response->body());
  246. throw $e;
  247. }
  248. }
  249. $cursor = $response["id"];
  250. $totalCount = intval($response["totalCount"]);
  251. $this->cursor = $cursor;
  252. if (0 < $totalCount) {
  253. $this->hasNext = true;
  254. $this->cursorDataCount = $totalCount;
  255. } else {
  256. $this->deleteCursor();
  257. }
  258. return $response;
  259. }
  260. /**
  261. * @return Collection<int,TValue>
  262. */
  263. public function next()
  264. {
  265. if (!$this->hasNext) {
  266. return collect();
  267. }
  268. $response = Http::withHeaders([
  269. "X-Cybozu-API-Token" => $this->apiToken,
  270. ])->get($this->getCursorUrl(), [
  271. "id" => $this->cursor,
  272. ]);
  273. if ($response->failed()) {
  274. $e = $response->toException();
  275. if ($e instanceof Exception) {
  276. Log::error($e->getMessage());
  277. throw $e;
  278. }
  279. }
  280. $ret = collect();
  281. $hasNext = $response["next"];
  282. if (!$hasNext) {
  283. $this->cursor = null;
  284. $this->hasNext = false;
  285. $this->cursorDataCount = 0;
  286. }
  287. foreach ($response["records"] as $data) {
  288. /**
  289. * @var KintoneModel $model
  290. */
  291. $model = new $this->appName();
  292. $model->setDataFromRecordResponse($data);
  293. $ret->push($model);
  294. }
  295. return $ret;
  296. }
  297. /**
  298. * @return Collection<int,TValue>
  299. */
  300. public function all(KintoneRecordQuery|null $query = null)
  301. {
  302. if ($this->cursor !== null) {
  303. $this->deleteCursor();
  304. }
  305. $this->createCursor($query);
  306. $list = collect();
  307. while (true) {
  308. $ret = $this->next();
  309. foreach ($ret as $ele) {
  310. $list->push($ele);
  311. }
  312. if ($this->cursor === null) {
  313. break;
  314. }
  315. }
  316. return $list;
  317. }
  318. public function deleteCursor()
  319. {
  320. if ($this->cursor === null) {
  321. return;
  322. }
  323. $response = Http::withHeaders([
  324. "X-Cybozu-API-Token" => $this->apiToken,
  325. ])->delete($this->getCursorUrl(), [
  326. "id" => $this->cursor,
  327. ]);
  328. if ($response->failed()) {
  329. $e = $response->toException();
  330. if ($e instanceof Exception) {
  331. Log::error($e->getMessage());
  332. throw $e;
  333. }
  334. }
  335. $this->cursor = null;
  336. $this->hasNext = false;
  337. $this->cursorDataCount = 0;
  338. }
  339. public function fileGet(string $fileKey)
  340. {
  341. $response = Http::withHeaders([
  342. "X-Cybozu-API-Token" => $this->apiToken,
  343. ])->get($this->getFileUrl(), [
  344. "fileKey" => $fileKey,
  345. ]);
  346. if ($response->failed()) {
  347. $e = $response->toException();
  348. if ($e instanceof Exception) {
  349. Log::error($e->getMessage());
  350. Log::error($response->body());
  351. throw $e;
  352. }
  353. }
  354. return $response;
  355. }
  356. /**
  357. * ファイルアップロード
  358. *
  359. * @param UploadedFile|TmpFile $file
  360. * @return string fileKey
  361. */
  362. public function filePut(UploadedFile|TmpFile $file): string
  363. {
  364. $content = "";
  365. $sendFileName = "";
  366. if ($file instanceof UploadedFile) {
  367. $content = file_get_contents($file);
  368. $sendFileName = sprintf("file.%s", $file->extension());
  369. } else if ($file instanceof TmpFile) {
  370. $content = $file->get();
  371. $sendFileName = $file->getAppFileName();
  372. }
  373. $response = Http::withHeaders([
  374. "X-Cybozu-API-Token" => $this->apiToken,
  375. ])
  376. ->attach("file", $content, $sendFileName)
  377. ->post($this->getFileUrl());
  378. if ($response->failed()) {
  379. $e = $response->toException();
  380. if ($e instanceof Exception) {
  381. Log::error($e->getMessage());
  382. Log::error($response->body());
  383. throw $e;
  384. }
  385. }
  386. return $response['fileKey'];
  387. }
  388. public function getAppFormFields(): array
  389. {
  390. $response = Http::withHeaders([
  391. "X-Cybozu-API-Token" => $this->apiToken,
  392. ])->get($this->getAppFormFieldsUrl(), [
  393. "app" => $this->appId,
  394. ]);
  395. if ($response->failed()) {
  396. $e = $response->toException();
  397. if ($e instanceof Exception) {
  398. Log::error($e->getMessage());
  399. throw $e;
  400. }
  401. }
  402. return $response['properties'];
  403. }
  404. private function getRecordUrl()
  405. {
  406. return $this->getUrl(self::URL_RECORD);
  407. }
  408. private function getRecordsUrl()
  409. {
  410. return $this->getUrl(self::URL_RECORDS);
  411. }
  412. private function getCursorUrl()
  413. {
  414. return $this->getUrl(self::URL_CURSOL);
  415. }
  416. private function getFileUrl()
  417. {
  418. return $this->getUrl(self::URL_FILE);
  419. }
  420. private function getAppFormFieldsUrl()
  421. {
  422. return $this->getUrl(self::URL_APP_FORM_FIELDS);
  423. }
  424. private function getUrl(string $path)
  425. {
  426. return $this->host . $path;
  427. }
  428. }