430 lines
19 KiB
PHP
430 lines
19 KiB
PHP
<?php
|
|
|
|
namespace App\Services;
|
|
|
|
use App\DTOs\CommonDTO;
|
|
use App\Entities\CommonEntity;
|
|
use App\Models\CommonModel;
|
|
use CodeIgniter\Database\Exceptions\DatabaseException;
|
|
use CodeIgniter\Validation\Exceptions\ValidationException;
|
|
use RuntimeException;
|
|
|
|
/**
|
|
* @template TEntity of CommonEntity
|
|
* @template TDto of CommonDTO
|
|
*/
|
|
abstract class CommonService
|
|
{
|
|
private array $_classPaths = [];
|
|
protected $title = null;
|
|
protected function __construct(protected CommonModel $model) {}
|
|
abstract public function action_init_process(string $action, array $formDatas = []): void;
|
|
abstract protected function getDTOClass(): string;
|
|
abstract public function createDTO(array $formDatas): CommonDTO;
|
|
abstract public function getEntityClass(): string;
|
|
abstract public function getFormService(): mixed;
|
|
abstract public function getHelper(): mixed;
|
|
final protected function addClassPaths(string $path): void
|
|
{
|
|
$this->_classPaths[] = $path;
|
|
}
|
|
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->model->getPKField();
|
|
$row = $this->model->selectMax($pkField)->get()->getRow();
|
|
return isset($row->{$pkField}) ? ((int)$row->{$pkField} + 1) : 1;
|
|
}
|
|
/**
|
|
* 단일 엔티티를 조회합니다.
|
|
* @return CommonEntity|null CommonEntity 인스턴스 또는 찾지 못했을 경우 null
|
|
*/
|
|
public function getEntity(string|int|array $where, ?string $message = null): ?CommonEntity
|
|
{
|
|
try {
|
|
$entity = is_array($where) ? $this->model->where($where)->first() : $this->model->find($where);
|
|
if (!$entity) {
|
|
return null;
|
|
}
|
|
if (!$entity instanceof CommonEntity) {
|
|
throw new \Exception(__METHOD__ . "에서 결과값 오류발생:\n" . var_export($entity, true));
|
|
}
|
|
return $this->getEntity_process($entity);
|
|
} catch (DatabaseException $e) {
|
|
$errorMessage = sprintf(
|
|
"\n------DB Query 오류 (%s)-----\nQuery: %s\nError: %s\n------------------------------\n",
|
|
__FUNCTION__,
|
|
$this->model->getLastQuery() ?? "No Query Available",
|
|
$e->getMessage()
|
|
);
|
|
log_message('error', $errorMessage);
|
|
throw new RuntimeException($errorMessage, $e->getCode(), $e);
|
|
} catch (\Exception $e) {
|
|
$errorMessage = sprintf(
|
|
"\n------일반 오류 (%s)-----\nError: %s\n------------------------------\n",
|
|
__FUNCTION__,
|
|
$e->getMessage()
|
|
);
|
|
throw new \Exception($errorMessage, $e->getCode(), $e);
|
|
}
|
|
}
|
|
public function getEntities(?array $where = null, array $columns = ['*']): array
|
|
{
|
|
try {
|
|
$entities = $this->getEntities_process($where, $columns);
|
|
log_message('debug', $this->model->getLastQuery());
|
|
return $entities;
|
|
} catch (DatabaseException $e) {
|
|
$errorMessage = sprintf(
|
|
"\n------DB Query 오류 (%s)-----\nQuery: %s\nError: %s\n------------------------------\n",
|
|
__FUNCTION__,
|
|
$this->model->getLastQuery() ?? "No Query Available",
|
|
$e->getMessage()
|
|
);
|
|
log_message('error', $errorMessage);
|
|
throw new RuntimeException($errorMessage, $e->getCode(), $e);
|
|
} catch (\Exception $e) {
|
|
$errorMessage = sprintf(
|
|
"\n------일반 오류 (%s)-----\nError: %s\n------------------------------\n",
|
|
__FUNCTION__,
|
|
$e->getMessage()
|
|
);
|
|
throw new \Exception($errorMessage, $e->getCode(), $e);
|
|
}
|
|
}
|
|
//Entity관련
|
|
protected function getEntity_process(CommonEntity $entity): CommonEntity
|
|
{
|
|
return $entity;
|
|
}
|
|
|
|
//entities를 가져오는 조건
|
|
protected function getEntities_process(mixed $where = null, array $columns = ['*']): array
|
|
{
|
|
if ($where) {
|
|
$this->model->where($where);
|
|
}
|
|
$entities = [];
|
|
foreach ($this->model->select(implode(',', $columns))->findAll() as $entity) {
|
|
$entities[$entity->getPK()] = $this->getEntity_process($entity);
|
|
}
|
|
return $entities;
|
|
}
|
|
|
|
//CURD 결과처리용
|
|
protected function handle_save_result(mixed $result, int|string $uid): int|string
|
|
{
|
|
log_message('debug', $this->model->getLastQuery());
|
|
if ($result === false) {
|
|
$errors = $this->model->errors();
|
|
$errorMsg = is_array($errors) ? implode(", ", $errors) : "DB 저장 작업이 실패했습니다.";
|
|
throw new RuntimeException(__METHOD__ . "에서 오류발생: " . $errorMsg);
|
|
}
|
|
$pk = $uid; // 기본적으로 기존 $uid (업데이트의 경우)
|
|
// AUTO_INCREMENT 필드를 사용하는 경우, INSERT 작업이라면 새로 생성된 ID를 가져옵니다.
|
|
// INSERT 작업은 보통 $uid가 0 또는 null/빈 문자열일 때 실행됩니다.
|
|
if ($this->model->useAutoIncrement() && (empty($uid) || $uid === 0)) {
|
|
// CodeIgniter 모델의 getInsertID()를 사용하여 새로 생성된 PK를 확실히 가져옵니다.
|
|
$insertID = $this->model->getInsertID();
|
|
if ($insertID > 0) {
|
|
$pk = $insertID;
|
|
}
|
|
} elseif ($this->model->useAutoIncrement() && is_numeric($result) && (int)$result > 0) {
|
|
// save()가 성공적인 INSERT 후 PK를 반환하는 경우를 대비 (CI4의 동작)
|
|
$pk = (int)$result;
|
|
}
|
|
// 최종적으로 PK가 유효한지 확인합니다.
|
|
if (empty($pk)) {
|
|
$errors = $this->model->errors();
|
|
$errorMsg = is_array($errors) && !empty($errors) ? implode(", ", $errors) : "DB 작업 성공 후 PK를 확인할 수 없거나 모델 오류 발생:{$pk}";
|
|
throw new RuntimeException(__METHOD__ . "에서 오류발생: " . $errorMsg);
|
|
}
|
|
return $pk;
|
|
}
|
|
|
|
protected function save_process(CommonEntity $entity): CommonEntity
|
|
{
|
|
// INSERT 시 Entity의 PK는 0 또는 NULL이어야 함 (DB가 ID를 생성하도록)
|
|
$initialPK = $entity->getPK();
|
|
$result = $this->model->save($entity);
|
|
// 최종적으로 DB에 반영된 PK를 반환받습니다. (UPDATE이면 기존 PK, INSERT이면 새 PK)
|
|
$entity->{$this->model->getPKField()} = $this->handle_save_result($result, $initialPK);
|
|
// handle_save_result에서 확인된 최종 PK를 사용하여 DB에서 최신 엔티티를 가져옴
|
|
return $entity;
|
|
}
|
|
|
|
//생성용
|
|
protected function create_process(array $formDatas): CommonEntity
|
|
{
|
|
// 💡 동적으로 가져온 Entity 클래스 이름으로 instanceof 검사
|
|
$entityClass = $this->getEntityClass();
|
|
$entity = new $entityClass($formDatas);
|
|
if (!$entity instanceof $entityClass) {
|
|
throw new RuntimeException(__METHOD__ . "에서 오류발생:Return Type은 {$entityClass}만 가능");
|
|
}
|
|
return $this->save_process($entity);
|
|
}
|
|
final public function create(object $dto): CommonEntity
|
|
{
|
|
$db = \Config\Database::connect();
|
|
try {
|
|
//트랜잭션 도중 DB 오류가 발생하면 DatabaseException을 던지도록 설정
|
|
$db->transException(true)->transStart();
|
|
//DTO 타입 체크 로직을 일반화
|
|
$dtoClass = $this->getDTOClass();
|
|
if (!$dto instanceof $dtoClass) {
|
|
throw new RuntimeException(__METHOD__ . "에서 오류발생: " . get_class($dto) . "는 사용할 수 없습니다. ({$dtoClass} 필요)");
|
|
}
|
|
$formDatas = $dto->toArray();
|
|
if (!$this->getFormService()->validate($formDatas)) {
|
|
throw new ValidationException(implode("\n", service('validation')->getErrors()));
|
|
}
|
|
// NOTE: create_process에서 엔티티를 생성할 때, 자동 증가(AUTO_INCREMENT) 필드는
|
|
// DB가 처리하도록 NULL이나 빈 값(0)으로 두는 것이 일반적입니다.
|
|
$entity = $this->create_process($formDatas);
|
|
// 트랜잭션 완료 및 커밋
|
|
$db->transComplete();
|
|
return $entity;
|
|
} catch (DatabaseException $e) {
|
|
// DatabaseException을 포착하면 자동으로 롤백 처리됨
|
|
throw new RuntimeException(sprintf(
|
|
"\n----[%s]에서 트랜잭션 실패: DB 오류----\n%s\n%s\n------------------------------\n",
|
|
__METHOD__,
|
|
$this->model->getLastQuery(),
|
|
$e->getMessage()
|
|
), $e->getCode(), $e);
|
|
} catch (\Throwable $e) {
|
|
$db->transRollback(); // 예외 발생 시 수동으로 롤백
|
|
throw new RuntimeException($e->getMessage(), 0, $e);
|
|
}
|
|
}
|
|
|
|
//수정용
|
|
protected function modify_process(string|int $uid, array $formDatas): CommonEntity
|
|
{
|
|
$entity = $this->getEntity($uid);
|
|
if (!$entity) {
|
|
throw new \Exception(__METHOD__ . "에서 오류발생: {$uid}에 해당하는 정보을 찾을수 없습니다.");
|
|
}
|
|
$pkField = $this->model->getPKField();
|
|
// DTO 데이터에서 PK 필드가 있다면 제거하여, fill()에서 기존 PK를 덮어쓰지 않도록 합니다.
|
|
if (isset($formDatas[$pkField])) {
|
|
unset($formDatas[$pkField]);
|
|
}
|
|
// 1. 데이터를 Entity에 채웁니다.
|
|
$entity->fill($formDatas);
|
|
// 2. (핵심 방어) fill() 작업이 Entity의 PK를 훼손했더라도,
|
|
// 기존 $uid 값을 사용하여 Entity의 PK를 강제로 복원합니다.
|
|
// 이것이 Model::save()가 UPDATE를 실행하도록 보장하는 최종 방어선입니다.
|
|
$currentPK = $entity->getPK();
|
|
if ($currentPK != $uid) {
|
|
log_message('warning', "modify_process: Entity PK 훼손 감지. '{$currentPK}' 대신 원본 UID '{$uid}'로 강제 복원.");
|
|
$entity->{$pkField} = $uid;
|
|
}
|
|
log_message('debug', "save_process 진입 전 Entity PK: " . $entity->getPK() . " (기대값: {$uid})");
|
|
return $this->save_process($entity);
|
|
}
|
|
final public function modify(string|int $uid, object $dto): CommonEntity
|
|
{
|
|
$db = \Config\Database::connect();
|
|
try {
|
|
//트랜잭션 도중 DB 오류가 발생하면 DatabaseException을 던지도록 설정
|
|
$db->transException(true)->transStart();
|
|
//DTO 타입 체크 로직을 일반화
|
|
$dtoClass = $this->getDTOClass();
|
|
if (!$dto instanceof $dtoClass) {
|
|
throw new RuntimeException(__METHOD__ . "에서 오류발생: " . get_class($dto) . "는 사용할 수 없습니다. ({$dtoClass} 필요)");
|
|
}
|
|
$formDatas = $dto->toArray();
|
|
if (!$this->getFormService()->validate($formDatas)) {
|
|
throw new ValidationException(implode("\n", service('validation')->getErrors()));
|
|
}
|
|
$entity = $this->modify_process($uid, $formDatas);
|
|
// 트랜잭션 완료 및 커밋
|
|
$db->transComplete();
|
|
return $entity;
|
|
} catch (DatabaseException $e) {
|
|
// DatabaseException을 포착하면 자동으로 롤백 처리됨
|
|
throw new RuntimeException(sprintf(
|
|
"\n----[%s]에서 트랜잭션 실패: DB 오류----\n%s\n%s\n------------------------------\n",
|
|
__METHOD__,
|
|
$this->model->getLastQuery(),
|
|
$e->getMessage()
|
|
), $e->getCode(), $e);
|
|
} catch (\Throwable $e) {
|
|
$db->transRollback(); // 예외 발생 시 수동으로 롤백
|
|
throw new RuntimeException($e->getMessage(), 0, $e);
|
|
}
|
|
}
|
|
|
|
//배치 작업용 수정
|
|
protected function batchjob_process(string|int $uid, array $formDatas): CommonEntity
|
|
{
|
|
// modify_process를 호출하여 로직 재사용 (PK 로드 및 PK 제거/방어 로직 포함)
|
|
$entity = $this->modify_process($uid, $formDatas);
|
|
return $entity;
|
|
}
|
|
final public function batchjob(array $uids, object $dto): array
|
|
{
|
|
$db = \Config\Database::connect();
|
|
try {
|
|
//트랜잭션 도중 DB 오류가 발생하면 DatabaseException을 던지도록 설정
|
|
$db->transException(true)->transStart();
|
|
//DTO 타입 체크 로직을 일반화
|
|
$dtoClass = $this->getDTOClass();
|
|
if (!$dto instanceof $dtoClass) {
|
|
throw new RuntimeException(__METHOD__ . "에서 오류발생: " . get_class($dto) . "는 사용할 수 없습니다. ({$dtoClass} 필요)");
|
|
}
|
|
$formDatas = $dto->toArray();
|
|
if (!$this->getFormService()->validate($formDatas)) {
|
|
throw new ValidationException(implode("\n", service('validation')->getErrors()));
|
|
}
|
|
//일괄작업처리
|
|
$entities = [];
|
|
foreach ($uids as $uid) {
|
|
$entities[] = $this->batchjob_process($uid, $formDatas);
|
|
}
|
|
// 트랜잭션 완료 및 커밋
|
|
$db->transComplete();
|
|
return $entities;
|
|
} catch (DatabaseException $e) {
|
|
// DatabaseException을 포착하면 자동으로 롤백 처리됨
|
|
throw new RuntimeException(sprintf(
|
|
"\n----[%s]에서 트랜잭션 실패: DB 오류----\n%s\n%s\n------------------------------\n",
|
|
__METHOD__,
|
|
$this->model->getLastQuery(),
|
|
$e->getMessage()
|
|
), $e->getCode(), $e);
|
|
} catch (\Throwable $e) {
|
|
$db->transRollback(); // 예외 발생 시 수동으로 롤백
|
|
throw new RuntimeException($e->getMessage(), 0, $e);
|
|
}
|
|
}
|
|
|
|
//삭제용 (일반)
|
|
protected function delete_process(string|int $uid): CommonEntity
|
|
{
|
|
if (!$uid) {
|
|
throw new \Exception("삭제에 필요한 PrimaryKey 가 정의 되지 않았습니다.");
|
|
}
|
|
$entity = $this->getEntity($uid);
|
|
if (!$entity) {
|
|
throw new \Exception(__METHOD__ . "에서 오류발생: {$uid}에 해당하는 정보을 찾을수 없습니다.");
|
|
}
|
|
$result = $this->model->delete($entity->getPK());
|
|
log_message('debug', $this->model->getLastQuery());
|
|
if ($result === false) {
|
|
$errors = $this->model->errors();
|
|
$errorMsg = is_array($errors) ? implode(", ", $errors) : "삭제 작업이 실패했습니다.";
|
|
throw new RuntimeException(__METHOD__ . "에서 오류발생: " . $errorMsg);
|
|
}
|
|
return $entity;
|
|
}
|
|
final public function delete(string|int $uid): CommonEntity
|
|
{
|
|
$db = \Config\Database::connect();
|
|
try {
|
|
//트랜잭션 도중 DB 오류가 발생하면 DatabaseException을 던지도록 설정
|
|
$db->transException(true)->transStart();
|
|
$entity = $this->delete_process($uid);
|
|
// 트랜잭션 완료 및 커밋
|
|
$db->transComplete();
|
|
return $entity;
|
|
} catch (DatabaseException $e) {
|
|
// DatabaseException을 포착하면 자동으로 롤백 처리됨
|
|
throw new RuntimeException(sprintf(
|
|
"\n----[%s]에서 트랜잭션 실패: DB 오류----\n%s\n%s\n------------------------------\n",
|
|
__METHOD__,
|
|
$this->model->getLastQuery(),
|
|
$e->getMessage()
|
|
), $e->getCode(), $e);
|
|
} catch (\Throwable $e) {
|
|
$db->transRollback(); // 예외 발생 시 수동으로 롤백
|
|
throw new RuntimeException($e->getMessage(), 0, $e);
|
|
}
|
|
}
|
|
|
|
//삭제용 (배치 작업)
|
|
protected function batchjob_delete_process(string|int $uid): CommonEntity
|
|
{
|
|
// delete_process를 호출하여 로직 재사용 (CommonEntity 로드 및 유효성 검사)
|
|
$entity = $this->delete_process($uid);
|
|
return $entity;
|
|
}
|
|
|
|
final public function batchjob_delete(array $uids): array
|
|
{
|
|
$db = \Config\Database::connect();
|
|
try {
|
|
//트랜잭션 도중 DB 오류가 발생하면 DatabaseException을 던지도록 설정
|
|
$db->transException(true)->transStart();
|
|
//일괄작업처리
|
|
$entities = [];
|
|
foreach ($uids as $uid) {
|
|
$entities[] = $this->batchjob_delete_process($uid);
|
|
}
|
|
// 트랜잭션 완료 및 커밋
|
|
$db->transComplete();
|
|
return $entities;
|
|
} catch (DatabaseException $e) {
|
|
// DatabaseException을 포착하면 자동으로 롤백 처리됨
|
|
throw new RuntimeException(sprintf(
|
|
"\n----[%s]에서 트랜잭션 실패: DB 오류----\n%s\n%s\n------------------------------\n",
|
|
__METHOD__,
|
|
$this->model->getLastQuery(),
|
|
$e->getMessage()
|
|
), $e->getCode(), $e);
|
|
} catch (\Throwable $e) {
|
|
$db->transRollback(); // 예외 발생 시 수동으로 롤백
|
|
throw new RuntimeException($e->getMessage(), 0, $e);
|
|
}
|
|
}
|
|
|
|
//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->model->getTitleField(), $word, 'both');
|
|
}
|
|
//날자검색
|
|
public function setDateFilter(string $start, string $end): void
|
|
{
|
|
$this->model->where(sprintf("%s.created_at >= '%s 00:00:00'", $this->model->getTable(), $start));
|
|
$this->model->where(sprintf("%s.created_at <= '%s 23:59:59'", $this->model->getTable(), $end));
|
|
}
|
|
//OrderBy 처리
|
|
public function setOrderBy(mixed $field = null, mixed $value = null): void
|
|
{
|
|
if ($field !== null && $value !== null) {
|
|
$this->model->orderBy(sprintf("%s.%s %s", $this->model->getTable(), $field, $value));
|
|
} else {
|
|
$this->model->orderBy(sprintf("%s.%s %s", $this->model->getTable(), $this->model->getPKField(), "DESC"));
|
|
}
|
|
}
|
|
}
|