pdo = $pdo; } final public function setDebug(bool $debug): void { $this->_debug = $debug; } final public function getLastQuery(): string { return $this->latestQuery; } final public function table(string $table): static { $this->table = $table; return $this; } protected function buildSelectSql(?string $select = null): string { $select = $select ?? implode(', ', $this->select); $sql = "SELECT {$select} FROM {$this->table}"; // ⬇️ Join 처리 추가 if (!empty($this->joins)) { $sql .= ' ' . implode(' ', $this->joins); } if (!empty($this->where)) { $sql .= " WHERE " . $this->buildWhereSql(); } if (!empty($this->order)) { $sql .= " ORDER BY " . implode(', ', $this->order); } if ($this->limit !== null) { $sql .= " LIMIT {$this->limit}"; } if ($this->offset !== null) { $sql .= " OFFSET {$this->offset}"; } return $sql; } protected function buildWhereSql(): string { $sql = ''; foreach ($this->where as $index => $clause) { if (is_array($clause)) { $boolean = $index === 0 ? '' : $clause['boolean'] . ' '; $sql .= $boolean . $clause['condition'] . ' '; } else { // 문자열 형태일 경우 (like에서 사용됨) $boolean = $index === 0 ? '' : 'AND '; $sql .= $boolean . $clause . ' '; } } return trim($sql); } //Select부분분 final public function select(array|string $columns): static { $this->select = is_array($columns) ? $columns : explode(',', $columns); return $this; } //Where절부분 final public function where(array|string $column, mixed $value = null, ?string $operator = null, string $boolean = "AND"): static { if (is_array($column)) { foreach ($column as $col => $val) { $this->where($col, $val); // 재귀 호출 } return $this; } // where("col = NOW()") 형태의 raw 처리 if ($value === null && $operator === null) { $this->where[] = ['condition' => $column, 'boolean' => $boolean]; return $this; } // where("col", "val") → operator 생략 시 = 처리 if ($operator === null) { $operator = "="; } $placeholder = ':w_' . count($this->bindings); $this->where[] = ['condition' => "$column $operator $placeholder", 'boolean' => $boolean]; $this->bindings[$placeholder] = $value; return $this; } final public function orWhere(array|string $column, mixed $value = null, ?string $operator = null, string $boolean = "OR"): static { return $this->where($column, $value, $operator, $boolean); } final public function whereIn(string $column, array $values, string $boolean = 'AND', $conditon = "IN"): static { if (empty($values)) { throw new \InvalidArgumentException(__FUNCTION__ . ": values 배열이 비어있을 수 없습니다."); } $placeholders = []; foreach ($values as $index => $value) { $placeholder = ":in_" . count($this->bindings); $placeholders[] = $placeholder; $this->bindings[$placeholder] = $value; } $condition = "$column $conditon (" . implode(', ', $placeholders) . ")"; $this->where[] = ['condition' => $condition, 'boolean' => $boolean]; return $this; } final public function whereNotIn(string $column, array $values, $boolean = "AND", $conditon = "NOT IN"): static { if (empty($values)) { throw new \InvalidArgumentException(__FUNCTION__ . ": values 배열이 비어있을 수 없습니다."); } return $this->whereIn($column, $values, $boolean, $conditon); } final public function orWhereIn(string $column, array $values, $boolean = "OR", $conditon = "IN"): static { if (empty($values)) { throw new \InvalidArgumentException(__FUNCTION__ . ": values 배열이 비어있을 수 없습니다."); } return $this->whereIn($column, $values, $boolean, $conditon); } //Join final public function join(string $table, string $on, string $type = 'INNER'): static { $this->joins[] = strtoupper($type) . " JOIN $table ON $on"; return $this; } //Like //사용예: // $model->like(['name' => '%홍%', 'email' => '%@naver.com%']); // $model->likeIn(['title', 'description'], '%공지%'); // $model->orLikeIn(['name', 'nickname'], '%철수%'); public function like(array|string $column, ?string $value = null, string $operator = 'LIKE', string $boolean = "AND"): static { if (is_array($column)) { $conditions = []; foreach ($column as $col => $val) { $placeholder = ':l_' . count($this->bindings); $conditions[] = "$col $operator $placeholder"; $this->bindings[$placeholder] = $val; } $this->where[] = '(' . implode(" $boolean ", $conditions) . ')'; } else { $placeholder = ':l_' . count($this->bindings); $condition = "$column $operator $placeholder"; if (empty($this->where)) { $this->where[] = $condition; } else { $last = array_pop($this->where); $this->where[] = "($last $boolean $condition)"; } $this->bindings[$placeholder] = $value; } return $this; } public function orLike(array|string $column, ?string $value = null, string $operator = 'LIKE', string $boolean = "OR"): static { return $this->like($column, $value, $operator, $boolean); } public function likeIn(array $columns, string $value, string $operator = "LIKE", string $boolean = "AND"): static { $orConditions = []; foreach ($columns as $col) { $placeholder = ':li_' . count($this->bindings); $orConditions[] = "$col $operator $placeholder"; $this->bindings[$placeholder] = $value; } $this->where[] = '(' . implode(" $boolean ", $orConditions) . ')'; return $this; } public function orLikeIn(array $columns, string $value, string $operator = "LIKE", string $boolean = "OR"): static { return $this->likeIn($columns, $value, $operator, $boolean); } public function notLikeIn(array $columns, string $value, string $operator = "NOT LIKE", string $boolean = "AND"): static { return $this->likeIn($columns, $value, $operator, $boolean); } //Order final public function orderBy(mixed $column, string $direction = 'ASC'): static { if (is_array($column)) { // 배열 형식의 여러 컬럼 정렬을 처리 foreach ($column as $col => $dir) { // 배열 키가 컬럼 이름이고 값이 방향임 $this->order[] = "$col $dir"; } } else { // 단일 컬럼에 대한 정렬 $this->order[] = "$column $direction"; } return $this; } //Limit부분 final public function limit(int $limit): static { $this->limit = $limit; return $this; } final public function offset(int $offset): static { $this->offset = $offset; return $this; } //Customer SQL 실행부분 final public function raw(string $sql, array $bindings = []): array { $stmt = $this->pdo->prepare($sql); foreach ($bindings as $key => $value) { $stmt->bindValue(is_int($key) ? $key + 1 : $key, $value); } $stmt->execute(); return $stmt->fetchAll(PDO::FETCH_ASSOC); } //Result부분 //binding까지 완료한 SQL문을 return함 final public function toRawSql(?string $select = null): string { $raw = $this->buildSelectSql($select); foreach ($this->bindings as $key => $value) { $escaped = is_numeric($value) ? $value : $this->pdo->quote($value); // 명명된 바인딩 if (str_starts_with($key, ':')) { $raw = str_replace($key, $escaped, $raw); } else { // 물음표 바인딩인 경우 (whereIn 등) $raw = preg_replace('/\?/', $escaped, $raw, 1); } } return $raw; } private function execute(?string $select = null): PDOStatement { if ($this->_debug) { echo php_sapi_name() === 'cli' ? "\nSQL DEBUG: " . $this->toRawSql($select) . "\n" : "
SQL DEBUG: " . $this->toRawSql($select) . ""; } $this->latestQuery = $this->toRawSql(); $stmt = $this->pdo->prepare($this->buildSelectSql($select)); foreach ($this->bindings as $key => $value) { $stmt->bindValue($key, $value); } $stmt->execute(); return $stmt; } final public function get(): array { $stmt = $this->execute(); $this->where = []; $this->bindings = []; return $stmt->fetchAll(PDO::FETCH_ASSOC); } final public function first(): ?array { $this->limit = 1; $results = $this->get(); return $results[0] ?? null; } final public function count(string $select = "COUNT(*) as cnt"): int { $stmt = $this->execute($select); $results = $stmt->fetchAll(PDO::FETCH_ASSOC); return (int)($results[0]['cnt'] ?? 0); } //CUD부분 final public function insert(array $data): bool { $columns = array_keys($data); $placeholders = array_map(fn($c) => ':' . $c, $columns); $sql = "INSERT INTO {$this->table} (" . implode(',', $columns) . ") VALUES (" . implode(',', $placeholders) . ")"; $stmt = $this->pdo->prepare($sql); foreach ($data as $col => $val) { $stmt->bindValue(':' . $col, $val); } return $stmt->execute(); } final public function update(array $data): bool { if (empty($this->where)) throw new \Exception("Update without WHERE is not allowed."); $setParts = []; foreach ($data as $col => $val) { $placeholder = ':u_' . $col; $setParts[] = "$col = $placeholder"; $this->bindings[$placeholder] = $val; } $sql = "UPDATE {$this->table} SET " . implode(', ', $setParts) . " WHERE " . implode(' AND ', $this->where); $stmt = $this->pdo->prepare($sql); foreach ($this->bindings as $k => $v) { $stmt->bindValue($k, $v); } return $stmt->execute(); } final public function delete(): bool { if (empty($this->where)) throw new \Exception("Delete without WHERE is not allowed."); $sql = "DELETE FROM {$this->table} WHERE " . implode(' AND ', $this->where); $stmt = $this->pdo->prepare($sql); foreach ($this->bindings as $k => $v) { $stmt->bindValue($k, $v); } return $stmt->execute(); } //transaction관련련 final public function beginTransaction(): void { $this->pdo->beginTransaction(); } final public function commit(): void { $this->pdo->commit(); } final public function rollBack(): void { if ($this->pdo->inTransaction()) { $this->pdo->rollBack(); } } }