516 lines
18 KiB
PHP
516 lines
18 KiB
PHP
<?php
|
|
|
|
namespace App\Services;
|
|
|
|
use RuntimeException;
|
|
use App\Forms\CommonForm;
|
|
use App\Models\CommonModel;
|
|
use App\Entities\CommonEntity;
|
|
use App\Libraries\AuthContext;
|
|
use App\Exceptions\FormValidationException;
|
|
use CodeIgniter\Database\Exceptions\DatabaseException;
|
|
|
|
abstract class CommonService
|
|
{
|
|
private ?AuthContext $_authContext = null;
|
|
private array $_classPaths = [];
|
|
protected $title = null;
|
|
|
|
protected ?object $_form = null;
|
|
protected ?object $_helper = null;
|
|
|
|
protected string $formClass = '';
|
|
protected string $helperClass = '';
|
|
|
|
protected function __construct(protected CommonModel $model)
|
|
{
|
|
}
|
|
|
|
abstract public function getEntityClass(): string;
|
|
|
|
/**
|
|
* 공통 트랜잭션 래퍼
|
|
*/
|
|
final protected function dbTransaction(callable $callback, string $functionName = ''): mixed
|
|
{
|
|
$db = \Config\Database::connect();
|
|
|
|
try {
|
|
$db->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');
|
|
}
|
|
}
|
|
} |