transException(true)->transStart(); $result = $callback($db); $db->transComplete(); return $result; } catch (FormValidationException $e) { // 검증 에러는 감싸지 말고 그대로 던져야 Ajax가 422 + errors 받음 $db->transRollback(); throw $e; } catch (DatabaseException $e) { $db->transRollback(); $errorMessage = sprintf( "\n----[%s]에서 트랜잭션 실패: DB 오류----\n%s\n%s\n------------------------------\n", static::class . '->' . ($functionName ?: 'dbTransaction'), $this->model->getLastQuery() ?? 'No Query Available', $e->getMessage() ); log_message('error', $errorMessage); throw new RuntimeException($errorMessage, $e->getCode(), $e); } catch (\Throwable $e) { $db->transRollback(); throw new RuntimeException($e->getMessage(), $e->getCode(), $e); } } final public function getActionForm(): mixed { if ($this->_form === null && $this->formClass) { $this->_form = new $this->formClass(); if (method_exists($this->_form, 'setAttributes')) { $this->_form->setAttributes([ 'pk_field' => $this->getPKField(), 'title_field' => $this->getTitleField(), 'table' => $this->model->getTable(), 'useAutoIncrement' => $this->model->useAutoIncrement(), 'class_path' => $this->getClassPaths(false), ]); } } return $this->_form; } public function getHelper(): mixed { if ($this->_helper === null && $this->helperClass) { $this->_helper = new $this->helperClass(); if (method_exists($this->_helper, 'setAttributes')) { $this->_helper->setAttributes([ 'pk_field' => $this->getPKField(), 'title_field' => $this->getTitleField(), 'table' => $this->model->getTable(), 'useAutoIncrement' => $this->model->useAutoIncrement(), 'class_path' => $this->getClassPaths(false), ]); } } return $this->_helper; } final public function getAuthContext(): AuthContext { if ($this->_authContext === null) { $this->_authContext = new AuthContext(); } return $this->_authContext; } final protected function addClassPaths(string $path): void { $this->_classPaths[] = $path; } final public function getPKField(): string { return $this->model->getPKField(); } final public function getTitleField(): string { return $this->model->getTitleField(); } final public function getClassPaths($isArray = true, $delimeter = DIRECTORY_SEPARATOR): array|string { return $isArray ? $this->_classPaths : implode($delimeter, $this->_classPaths); } final public function getNextPK(): int { $pkField = $this->getPKField(); $row = $this->model->selectMax($pkField)->get()->getRow(); return isset($row->{$pkField}) ? ((int) $row->{$pkField} + 1) : 1; } /** * 단일 엔티티를 조회합니다. */ abstract protected function getEntity_process($entity): CommonEntity; final public function getEntity($where = null, ?string $message = null): ?CommonEntity { try { $entity = null; if ($where !== null) { $entity = is_array($where) ? $this->model->where($where)->first() : $this->model->find($where); } if ($entity === null) { return null; } $entityClass = $this->getEntityClass(); if (!$entity instanceof $entityClass) { throw new RuntimeException( static::class . '->' . __FUNCTION__ . "에서 오류발생:Return Type은 {$entityClass}만 가능:" . get_class($entity) ); } return $this->getEntity_process($entity); } catch (DatabaseException $e) { $errorMessage = sprintf( "\n------DB Query 오류 (%s)-----\nQuery: %s\nError: %s\n------------------------------\n", static::class . '->' . __FUNCTION__, $this->model->getLastQuery() ?? 'No Query Available', $e->getMessage() ); log_message('error', $errorMessage); throw new RuntimeException($errorMessage, $e->getCode(), $e); } catch (\Throwable $e) { $errorMessage = sprintf( "\n------일반 오류 (%s)-----\nError: %s\n------------------------------\n", static::class . '->' . __FUNCTION__, $e->getMessage() ); throw new RuntimeException($errorMessage, $e->getCode(), $e); } } /** * 엔티티 배열 조회합니다. * @return CommonEntity[] CommonEntity 인스턴스 배열 */ protected function getEntities_process(mixed $where = null, array $columns = ['*'], array $entities = []): array { if ($where) { $this->model->where($where); } // 기본 Order By 이용 $this->setOrderBy(); if (!empty($columns) && $columns !== ['*']) { $this->model->select(implode(',', $columns)); } /** @var array<\App\Entities\CommonEntity> $results */ $results = $this->model->findAll(); log_message('debug', (string) $this->model->getLastQuery()); foreach ($results as $entity) { $entities[] = $entity; } return $entities; } public function getEntities(?array $where = null, array $columns = ['*'], array $entities = []): array { try { /** @var array<\App\Entities\CommonEntity> $entities */ $entities = $this->getEntities_process($where, $columns, $entities); return $entities; } catch (DatabaseException $e) { $errorMessage = sprintf( "\n------DB Query 오류 (%s)-----\nQuery: %s\nError: %s\n------------------------------\n", static::class . '->' . __FUNCTION__, $this->model->getLastQuery() ?? 'No Query Available', $e->getMessage() ); log_message('error', $errorMessage); throw new RuntimeException($errorMessage, $e->getCode(), $e); } catch (\Throwable $e) { $errorMessage = sprintf( "\n------일반 오류 (%s)-----\nError: %s\n------------------------------\n", static::class . '->' . __FUNCTION__, $e->getMessage() ); throw new RuntimeException($errorMessage, $e->getCode(), $e); } } // CURD 결과처리용 protected function save_process($entity): CommonEntity { try { if (!$this->model->save($entity)) { $errors = $this->model->errors(); $errorMsg = is_array($errors) ? implode(', ', $errors) : 'DB 저장 작업이 실패했습니다.'; throw new RuntimeException(static::class . '->' . __FUNCTION__ . '에서 오류발생: ' . $errorMsg); } return $entity; } catch (\Throwable $e) { log_message('debug', __FUNCTION__ . ':' . var_export($entity, true)); log_message('debug', __FUNCTION__ . ':' . (string) $this->model->getLastQuery()); throw new RuntimeException(static::class . '->' . __FUNCTION__ . '에서 오류발생:' . $e->getMessage()); } } // Action 작업시 field에따른 Hook처리(각 Service에서 override) protected function fieldhook_process(string $field, $value, array $formDatas): array { return $formDatas; } // 생성용 protected function create_process(array $formDatas): CommonEntity { try { $actionForm = $this->getActionForm(); if (!$actionForm instanceof CommonForm) { throw new RuntimeException(static::class . '->' . __FUNCTION__ . '에서 오류발생: actionForm이 정의되지 않았습니다.'); } // create 폼 초기화 $actionForm->form_init_process('create', $formDatas); // 검증 $actionForm->validate($formDatas); // 후처리 $processedDatas = $formDatas; foreach (array_keys($processedDatas) as $field) { $value = $processedDatas[$field] ?? null; $processedDatas = $this->fieldhook_process($field, $value, $processedDatas); } $entityClass = $this->getEntityClass(); $entity = new $entityClass($processedDatas); if (!$entity instanceof CommonEntity) { throw new RuntimeException('Return Type은 CommonEntity만 가능'); } if (!$entity instanceof $entityClass) { throw new RuntimeException("Return Type은 {$entityClass}만 가능"); } $entity = $this->save_process($entity); // 생성 PK 설정 if ($this->model->useAutoIncrement()) { $entity->{$this->getPKField()} = $this->model->getInsertID(); } return $entity; } catch (FormValidationException $e) { throw $e; } catch (\Throwable $e) { throw new RuntimeException(static::class . '->' . __FUNCTION__ . '에서 오류발생:' . $e->getMessage()); } } final public function create(array $formDatas): CommonEntity { return $this->dbTransaction(function () use ($formDatas) { $formDatas['user_uid'] = (int) $this->getAuthContext()->getUID(); return $this->create_process($formDatas); }, __FUNCTION__); } // 수정용 protected function modify_process($entity, array $formDatas): CommonEntity { try { $actionForm = $this->getActionForm(); if (!$actionForm instanceof CommonForm) { throw new RuntimeException( static::class . '->' . __FUNCTION__ . '에서 오류발생: actionForm이 정의되지 않았습니다.' ); } // 기존 엔티티 데이터 + 수정 데이터 병합 $validateFormDatas = $entity->toArray(); foreach ($formDatas as $key => $value) { $validateFormDatas[$key] = $value; } // 병합 데이터 기준으로 modify 폼 초기화 $actionForm->form_init_process('modify', $validateFormDatas); // 검증 $actionForm->validate($validateFormDatas); // 후처리 $processedDatas = $validateFormDatas; foreach (array_keys($processedDatas) as $field) { $value = $processedDatas[$field] ?? null; $processedDatas = $this->fieldhook_process($field, $value, $processedDatas); } $entity->fill($processedDatas); if ($entity->hasChanged()) { $entity = $this->save_process($entity); } return $entity; } catch (FormValidationException $e) { throw $e; } catch (\Throwable $e) { throw new RuntimeException( static::class . '->' . __FUNCTION__ . '에서 오류발생:' . $e->getMessage() ); } } final public function modify(string|int $uid, array $formDatas): CommonEntity { return $this->dbTransaction(function () use ($uid, $formDatas) { $entity = $this->getEntity($uid); if (!$entity) { throw new RuntimeException(static::class . '->' . __FUNCTION__ . "에서 오류발생: {$uid}에 해당하는 정보을 찾을수 없습니다."); } $formDatas['user_uid'] = (int) $this->getAuthContext()->getUID(); return $this->modify_process($entity, $formDatas); }, __FUNCTION__); } // 배치 작업용 수정 protected function batchjob_process($entity, array $formDatas): CommonEntity { $entity = $this->modify_process($entity, $formDatas); return $entity; } final public function batchjob(array $uids, array $formDatas): array { return $this->dbTransaction(function () use ($uids, $formDatas) { $entities = []; foreach ($uids as $uid) { $entity = $this->getEntity($uid); if (!$entity) { throw new RuntimeException(static::class . '->' . __FUNCTION__ . "에서 오류발생: {$uid}에 해당하는 정보을 찾을수 없습니다."); } $entityClass = $this->getEntityClass(); if (!$entity instanceof $entityClass) { throw new RuntimeException(static::class . '->' . __FUNCTION__ . "에서 오류발생:Return Type은 {$entityClass}만 가능"); } $formDatas['user_uid'] = (int) $this->getAuthContext()->getUID(); $entities[] = $this->batchjob_process($entity, $formDatas); } return $entities; }, __FUNCTION__); } // 삭제용 (일반) protected function delete_process($entity): CommonEntity { $result = $this->model->delete($entity->getPK()); if ($result === false) { $errors = $this->model->errors(); $errorMsg = is_array($errors) ? implode(', ', $errors) : '삭제 작업이 실패했습니다.'; throw new RuntimeException(static::class . '->' . __FUNCTION__ . '에서 오류발생: ' . $errorMsg); } return $entity; } final public function delete(string|int $uid): CommonEntity { return $this->dbTransaction(function () use ($uid) { if (!$uid) { throw new RuntimeException(static::class . '->' . __FUNCTION__ . '에서 삭제에 필요한 PrimaryKey 가 정의 되지 않았습니다.'); } $entity = $this->getEntity($uid); if (!$entity) { throw new RuntimeException(static::class . '->' . __FUNCTION__ . "에서 오류발생: {$uid}에 해당하는 정보을 찾을수 없습니다."); } $entityClass = $this->getEntityClass(); if (!$entity instanceof $entityClass) { throw new RuntimeException(static::class . '->' . __FUNCTION__ . "에서 오류발생:Return Type은 {$entityClass}만 가능"); } return $this->delete_process($entity); }, __FUNCTION__); } // 삭제용 (배치 작업) protected function batchjob_delete_process($entity): CommonEntity { $entity = $this->delete_process($entity); return $entity; } final public function batchjob_delete(array $uids): array { return $this->dbTransaction(function () use ($uids) { $entities = []; foreach ($uids as $uid) { $entity = $this->getEntity($uid); if (!$entity) { throw new RuntimeException(static::class . '->' . __FUNCTION__ . "에서 오류발생: {$uid}에 해당하는 정보을 찾을수 없습니다."); } $entityClass = $this->getEntityClass(); if (!$entity instanceof $entityClass) { throw new RuntimeException(static::class . '->' . __FUNCTION__ . "에서 오류발생:Return Type은 {$entityClass}만 가능"); } $entities[] = $this->batchjob_delete_process($entity); } return $entities; }, __FUNCTION__); } // Index용 final public function getTotalCount(): int { return $this->model->countAllResults(); } // Limit처리 final public function setLimit(int $perpage): void { $this->model->limit($perpage); } // Offset처리 final public function setOffset(int $offset): void { $this->model->offset($offset); } public function setFilter(string $field, mixed $filter_value): void { switch ($field) { default: $this->model->where("{$this->model->getTable()}.{$field}", $filter_value); break; } } // 검색어조건절처리 public function setSearchWord(string $word): void { $this->model->orLike($this->model->getTable() . '.' . $this->getTitleField(), $word, 'both'); } // 날짜검색 public function setDateFilter(string $start, string $end): void { $this->model->where("{$this->model->getTable()}.created_at >=", $start . ' 00:00:00'); $this->model->where("{$this->model->getTable()}.created_at <=", $end . ' 23:59:59'); } // OrderBy 처리 public function setOrderBy(mixed $field = null, mixed $value = null): void { if ($field !== null && $value !== null) { $this->model->orderBy($this->model->getTable() . '.' . $field, (string) $value); } else { $this->model->orderBy($this->model->getTable() . '.' . $this->getPKField(), 'DESC'); } } }