348 lines
10 KiB
PHP
348 lines
10 KiB
PHP
<?php
|
|
|
|
namespace lib\Database;
|
|
|
|
use PDO;
|
|
|
|
class QueryBuilder
|
|
{
|
|
private bool $_debug = false;
|
|
protected PDO $pdo;
|
|
protected string $latestQuery = "";
|
|
protected string $table = '';
|
|
protected array $select = ['*'];
|
|
protected array $where = [];
|
|
protected array $bindings = [];
|
|
protected array $order = [];
|
|
protected array $joins = [];
|
|
protected ?int $limit = null;
|
|
protected ?int $offset = null;
|
|
|
|
protected function __construct(PDO $pdo)
|
|
{
|
|
$this->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
|
|
{
|
|
$sql = "SELECT " . implode(', ', $this->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
|
|
{
|
|
$raw = $this->buildSelectSql();
|
|
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;
|
|
}
|
|
final public function get(): array
|
|
{
|
|
if ($this->_debug) {
|
|
echo php_sapi_name() === 'cli'
|
|
? "\nSQL DEBUG: " . $this->toRawSql() . "\n"
|
|
: "<pre>SQL DEBUG: " . $this->toRawSql() . "</pre>";
|
|
}
|
|
$this->latestQuery = $this->toRawSql();
|
|
$stmt = $this->pdo->prepare($this->buildSelectSql());
|
|
foreach ($this->bindings as $key => $value) {
|
|
$stmt->bindValue($key, $value);
|
|
}
|
|
$stmt->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
|
|
{
|
|
$this->select = [$select];
|
|
$results = $this->get();
|
|
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();
|
|
}
|
|
}
|
|
}
|