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.

553 line
14KB

  1. <?php
  2. namespace App\Kintone\Models;
  3. use App\Exceptions\ConfigException;
  4. use App\Files\TmpFile;
  5. use App\Kintone\File;
  6. use App\Kintone\KintoneAccess;
  7. use App\Kintone\KintoneAccessStore;
  8. use App\Kintone\KintoneRecordQuery;
  9. use App\Kintone\Models\SubTable\SubTableData;
  10. use App\Util\DateUtil;
  11. use Illuminate\Http\UploadedFile;
  12. use Illuminate\Support\Arr;
  13. use Illuminate\Support\Carbon;
  14. use Illuminate\Support\Collection;
  15. use Illuminate\Support\Str;
  16. use LogicException;
  17. use stdClass;
  18. abstract class KintoneModel
  19. {
  20. const CONFIG_KEY = "";
  21. const BASE_MODEL = null;
  22. static public function configKey(): string
  23. {
  24. if (!static::CONFIG_KEY) {
  25. throw new LogicException(sprintf("Kintone 設定キー 未設定:%s", static::class));
  26. }
  27. return static::CONFIG_KEY;
  28. }
  29. static public function setConfig(): array
  30. {
  31. $configStr = env(static::configKey());
  32. if (!$configStr) {
  33. throw new ConfigException(static::configKey(), $configStr);
  34. }
  35. $params = explode(",", $configStr);
  36. if (count($params) !== 2) {
  37. throw new ConfigException(static::configKey(), $configStr);
  38. }
  39. [$apiToken, $appId] = $params;
  40. return [static::class => [
  41. 'apiToken' => $apiToken,
  42. 'appId' => (int)$appId,
  43. ]];
  44. }
  45. /**
  46. * @return KintoneAccess<static>
  47. */
  48. static public function getAccess(): KintoneAccess
  49. {
  50. $target = static::BASE_MODEL ?? static::class;
  51. $store = KintoneAccessStore::instance();
  52. if ($store->has($target)) {
  53. return $store->get($target);
  54. }
  55. $access = new KintoneAccess($target, static::class);
  56. $access->setFields(array_keys(static::FIELDS));
  57. foreach (static::RELATIONS as $relation) {
  58. $access->addAppToken($relation);
  59. }
  60. $store->set($target, $access);
  61. return $access;
  62. }
  63. /**
  64. * @return KintoneRecordQuery<static>
  65. */
  66. static public function getQuery(): KintoneRecordQuery
  67. {
  68. $target = static::BASE_MODEL ?? static::class;
  69. return new KintoneRecordQuery($target);
  70. }
  71. /**
  72. * @return static
  73. */
  74. public static function find(int $recordId)
  75. {
  76. return static::getAccess()->find($recordId);
  77. }
  78. /**
  79. * @return static
  80. */
  81. public static function first(?KintoneRecordQuery $query = null)
  82. {
  83. return static::getAccess()->first($query);
  84. }
  85. public static function getDropDownOptions(string $fieldCode): array
  86. {
  87. $ret = [];
  88. $properties = static::getAccess()->getAppFormFields();
  89. $options = Arr::get($properties, sprintf("%s.options", $fieldCode));
  90. $ret = Arr::pluck(Arr::sort($options, function ($option) {
  91. return data_get($option, "index");
  92. }), "label");
  93. return $ret;
  94. }
  95. protected ?string $recordId = null;
  96. protected ?int $revision = null;
  97. protected ?Carbon $createdAt = null;
  98. protected ?Carbon $updatedAt = null;
  99. protected ?stdClass $dataOrigin = null;
  100. protected stdClass $data;
  101. const FIELD_RECORD_NO = "レコード番号";
  102. protected const FIELDS = [];
  103. protected const SUB_TABLES = [];
  104. protected const FIELD_NAMES = [];
  105. protected const RELATIONS = [];
  106. private array $changed = [];
  107. public function __construct()
  108. {
  109. $this->data = new stdClass();
  110. foreach (static::FIELDS as $fileCode => $type) {
  111. if ($type === FieldType::SUBTABLE) {
  112. $this->set($fileCode, collect());
  113. }
  114. }
  115. }
  116. public function __get($name)
  117. {
  118. $field = "FIELD_" . Str::of($name)->snake()->upper()->toString();
  119. $constantPath = sprintf("%s::%s", static::class, $field);
  120. if (defined($constantPath)) {
  121. return $this->get(constant($constantPath));
  122. } else {
  123. throw new LogicException(sprintf("未定義のメンバアクセス %s %s", static::class, $field));
  124. }
  125. }
  126. public function __set($name, $value)
  127. {
  128. $field = "FIELD_" . Str::of($name)->snake()->upper()->toString();
  129. $constantPath = sprintf("%s::%s", static::class, $field);
  130. if (defined($constantPath)) {
  131. return $this->set(constant($constantPath), $value);
  132. } else {
  133. throw new LogicException(sprintf("未定義のメンバアクセス %s %s", static::class, $field));
  134. }
  135. }
  136. public function set(string $fieldCode, $value)
  137. {
  138. $type = $this->getFieldType($fieldCode);
  139. if ($type) {
  140. $this->setData($fieldCode, $value);
  141. data_set($this->changed, $fieldCode, true);
  142. }
  143. return $this;
  144. }
  145. private function setData(string $path, $value)
  146. {
  147. data_set($this->data, $path, $value);
  148. return $this;
  149. }
  150. public function setDataFromRecordResponse(array $data): bool
  151. {
  152. $ret = true;
  153. foreach ($data as $fieldCode => $ele) {
  154. $type = data_get($ele, "type");
  155. $value = data_get($ele, "value");
  156. if ($type === "__ID__") {
  157. $this->recordId = $value;
  158. continue;
  159. }
  160. if ($type === "__REVISION__") {
  161. $this->revision = $value;
  162. continue;
  163. }
  164. if ($type === "CREATED_TIME") {
  165. $this->createdAt = DateUtil::parse($value);
  166. continue;
  167. }
  168. if ($type === "UPDATED_TIME") {
  169. $this->updatedAt = DateUtil::parse($value);
  170. continue;
  171. }
  172. data_set($this->data, $fieldCode, $this->readData($ele, $fieldCode));
  173. }
  174. if ($this->recordId === null) {
  175. throw new LogicException(sprintf("レコード番号取得失敗 :%s", static::class));
  176. }
  177. $ret = $this->setDataCustom($data);
  178. if ($ret) {
  179. $this->clean();
  180. }
  181. return $ret;
  182. }
  183. private function readData(array $data, string $fieldCode = "")
  184. {
  185. $type = data_get($data, "type");
  186. $value = data_get($data, "value");
  187. if ($type === null || $value === null) {
  188. return null;
  189. }
  190. $type = FieldType::tryFrom($type);
  191. if ($type !== null) {
  192. if (in_array($type, [FieldType::DATETIME, FieldType::DATE])) {
  193. if ($value) {
  194. return DateUtil::parse($value);
  195. } else {
  196. return null;
  197. }
  198. }
  199. if ($type === FieldType::NUMBER) {
  200. return intval($value);
  201. }
  202. if ($type === FieldType::FILE) {
  203. $ret = [];
  204. foreach ($value as $f) {
  205. $ret[] = new File($f);
  206. }
  207. return $ret;
  208. }
  209. if ($type === FieldType::SUBTABLE && $this->getFieldType($fieldCode)) {
  210. $ret = collect();
  211. foreach ($value as $v) {
  212. $rowRet = [];
  213. $rowArray = data_get($v, 'value');
  214. $isEmptyRow = true;
  215. foreach ($rowArray as $subFieldCode => $ele) {
  216. $rowRet[$subFieldCode] = $this->readData($ele);
  217. if (!!$rowRet[$subFieldCode]) {
  218. $isEmptyRow = false;
  219. }
  220. }
  221. if (!$isEmptyRow) {
  222. $ret->push($this->getSubTableData($fieldCode, $rowRet));
  223. }
  224. }
  225. return $ret;
  226. }
  227. return $value;
  228. }
  229. return null;
  230. }
  231. private function getSubTableData(string $fieldCode, array $data): SubTableData
  232. {
  233. $className = data_get(static::SUB_TABLES, $fieldCode, "");
  234. if (class_exists($className)) {
  235. return new $className($data);
  236. }
  237. throw new LogicException("キントーン設定不備");
  238. }
  239. /**
  240. * 変更前データを現在データで上書きする
  241. *
  242. * @return void
  243. */
  244. public function clean(?int $revision = null)
  245. {
  246. $this->dataOrigin = clone $this->data;
  247. if ($revision !== null) {
  248. $this->revision = $revision;
  249. }
  250. $this->changed = [];
  251. }
  252. public function getApiLayout(): array
  253. {
  254. $ret = [];
  255. foreach (static::FIELDS as $fieldCode => $type) {
  256. // 変更があった項目のみレイアウトへ出力する
  257. if (!Arr::has($this->changed, $fieldCode)) {
  258. continue;
  259. }
  260. $path = sprintf("%s.value", $fieldCode);
  261. data_set($ret, $path, $this->getApiLayoutValue($fieldCode));
  262. }
  263. return array_merge($ret, $this->getApiLayoutCustom());
  264. }
  265. private function getApiLayoutValue(string $fieldCode)
  266. {
  267. $ret = [];
  268. $data = $this->get($fieldCode);
  269. $type = $this->getFieldType($fieldCode);
  270. if ($type === FieldType::DATETIME) {
  271. $data = $this->getDate($fieldCode);
  272. if ($data) {
  273. return $data->toIso8601ZuluString();
  274. } else {
  275. return "";
  276. }
  277. }
  278. if ($type === FieldType::DATE) {
  279. $data = $this->getDate($fieldCode);
  280. if ($data) {
  281. return $data->toDateString();
  282. } else {
  283. return "";
  284. }
  285. }
  286. if ($data instanceof Collection) {
  287. $ret = [];
  288. foreach ($data as $ele) {
  289. if ($ele instanceof SubTableData) {
  290. $obj = [];
  291. $values = $ele->toArray();
  292. foreach ($values as $subFieldCode => $value) {
  293. $obj['value'][$subFieldCode]['value'] = $value;
  294. }
  295. $ret[] = $obj;
  296. }
  297. }
  298. return $ret;
  299. }
  300. return data_get($this->data, $fieldCode);
  301. }
  302. private function getFieldType(string $fieldCode): ?FieldType
  303. {
  304. return data_get(static::FIELDS, $fieldCode);
  305. }
  306. public function get(string $key)
  307. {
  308. return data_get($this->data, $key);
  309. }
  310. public function getStr(string $key): ?string
  311. {
  312. return $this->get($key);
  313. }
  314. public function getNumber(string $key): ?int
  315. {
  316. return $this->get($key);
  317. }
  318. public function getDate(string $key): ?Carbon
  319. {
  320. return $this->get($key);
  321. }
  322. public function getTable(string $key): ?array
  323. {
  324. return $this->get($key);
  325. }
  326. public function setRecordId(string $id): static
  327. {
  328. $this->recordId = $id;
  329. return $this;
  330. }
  331. public function getRecordId(): ?string
  332. {
  333. return $this->recordId;
  334. }
  335. public function getRevision(): ?int
  336. {
  337. return $this->revision;
  338. }
  339. public function getUpdatedAt(): ?Carbon
  340. {
  341. return $this->updatedAt;
  342. }
  343. public function getCreatedAt(): ?Carbon
  344. {
  345. return $this->createdAt;
  346. }
  347. /**
  348. * @param string $fieldCode
  349. * @param Collection<int, UploadedFile|TmpFile> $files
  350. * @param bool $override
  351. * @return static
  352. */
  353. public function setFiles(string $fieldCode, Collection $files, bool $override = true): static
  354. {
  355. if ($override) {
  356. $this->set($fieldCode, []);
  357. }
  358. foreach ($files as $file) {
  359. $this->addFile($fieldCode, $file);
  360. }
  361. return $this;
  362. }
  363. public function addFile(string $fieldCode, UploadedFile|TmpFile $file)
  364. {
  365. if ($file instanceof UploadedFile) {
  366. $name = sprintf("image_%d.%s", Str::uuid(), $file->extension());
  367. $contentType = $file->getClientMimeType();
  368. } else {
  369. $name = $file->getAppFileName();
  370. $contentType = $file->getMimeType();
  371. }
  372. $access = $this->getAccess();
  373. $field = $this->get($fieldCode);
  374. array_push($field, [
  375. 'fileKey' => $access->filePut($file),
  376. 'name' => $name,
  377. 'contentType' => $contentType,
  378. ]);
  379. $this->set($fieldCode, $field);
  380. return $this;
  381. }
  382. public function save()
  383. {
  384. if (count($this->changed) === 0) {
  385. return;
  386. }
  387. $access = static::getAccess();
  388. if ($this->recordId === null) {
  389. $access->create($this);
  390. } else {
  391. $access->update($this);
  392. }
  393. }
  394. public function toArray($column = ['*']): array
  395. {
  396. if ($this->recordId === null) {
  397. throw new LogicException("保存前モデルのシリアライズ検知");
  398. }
  399. $ret = [
  400. 'record_no' => $this->recordId,
  401. 'revision' => $this->revision,
  402. ];
  403. $columnAll = data_get($column, 0) === '*';
  404. /**
  405. * @var string $fieldCode
  406. */
  407. foreach ($this->data as $fieldCode => $value) {
  408. if (!$columnAll && !in_array($fieldCode, $column)) {
  409. continue;
  410. }
  411. $type = data_get(static::FIELDS, $fieldCode);
  412. $columnName = data_get(static::FIELD_NAMES, $fieldCode, null);
  413. if ($columnName === null) {
  414. continue;
  415. }
  416. if ($type === null) {
  417. $ret[$columnName] = $value;
  418. continue;
  419. }
  420. if ($type === FieldType::DATETIME) {
  421. if ($value instanceof Carbon) {
  422. $ret[$columnName] = $value->format('Y/m/d H:i');
  423. } else {
  424. $ret[$columnName] = $value;
  425. }
  426. continue;
  427. }
  428. if ($type === FieldType::DATE) {
  429. if ($value instanceof Carbon) {
  430. $ret[$columnName] = $value->format('Y/m/d');
  431. } else {
  432. $ret[$columnName] = $value;
  433. }
  434. continue;
  435. }
  436. $ret[$columnName] = $value;
  437. }
  438. $ret = array_merge($ret, $this->toArrayCustom());
  439. return $ret;
  440. }
  441. protected function toArrayCustom(): array
  442. {
  443. return [];
  444. }
  445. /**
  446. * オーバーライドを期待
  447. *
  448. * @param array $data
  449. * @return boolean
  450. */
  451. protected function setDataCustom(array $data): bool
  452. {
  453. return true;
  454. }
  455. /**
  456. * オーバーライドを期待
  457. *
  458. * @return array
  459. */
  460. protected function getApiLayoutCustom(): array
  461. {
  462. return [];
  463. }
  464. }