db = Database::getInstance(); if (empty($this->table)) { // Auto-generate table name from class name $className = (new \ReflectionClass($this))->getShortName(); $this->table = strtolower(preg_replace('/(?table} WHERE {$this->primaryKey} = ? LIMIT 1"; $result = $this->db->queryOne($sql, [$id]); return $result ? $this->castAttributes($result) : null; } /** * Find record by primary key or throw exception */ public function findOrFail($id): array { $result = $this->find($id); if (!$result) { throw new Exception("Record not found in {$this->table} with {$this->primaryKey} = {$id}"); } return $result; } /** * Find first record matching conditions */ public function first(): ?array { $this->limit = 1; $results = $this->get(); return $results[0] ?? null; } /** * Find first record or throw exception */ public function firstOrFail(): array { $result = $this->first(); if (!$result) { throw new Exception("No records found in {$this->table}"); } return $result; } /** * Get all records matching current query */ public function get(): array { $sql = $this->buildSelectQuery(); $params = $this->getWhereParams(); $results = $this->db->query($sql, $params); // Reset query builder $this->resetQuery(); return array_map([$this, 'castAttributes'], $results); } /** * Execute raw SQL query */ public function query(string $sql, array $params = []): array { return $this->db->query($sql, $params); } /** * Get all records from table */ public function all(): array { return $this->get(); } /** * Count records matching current query */ public function count(): int { $originalSelect = $this->select; $this->select = ['COUNT(*) as count']; $sql = $this->buildSelectQuery(); $params = $this->getWhereParams(); $result = $this->db->queryOne($sql, $params); // Reset query builder $this->select = $originalSelect; $this->resetQuery(); return (int) ($result['count'] ?? 0); } /** * Check if records exist matching current query */ public function exists(): bool { return $this->count() > 0; } /** * Create new record */ public function create(array $data): int { $data = $this->filterFillable($data); if ($this->timestamps) { $data['created_at'] = date('Y-m-d H:i:s'); $data['updated_at'] = date('Y-m-d H:i:s'); } $columns = array_keys($data); $placeholders = array_fill(0, count($columns), '?'); $sql = "INSERT INTO {$this->table} (".implode(', ', $columns).") VALUES (".implode(', ', $placeholders).")"; // Get the connection to ensure we use the same one for lastInsertId $pdo = $this->db->getConnection(); try { $stmt = $pdo->prepare($sql); $stmt->execute(array_values($data)); $lastId = $pdo->lastInsertId(); return (int) $lastId; } catch (Exception $e) { throw new Exception("Failed to create record in {$this->table}: ".$e->getMessage()); } } /** * Update records matching current query */ public function update(array $data): int { if (empty($this->wheres)) { throw new Exception("Cannot update without WHERE conditions for safety"); } $data = $this->filterFillable($data); if ($this->timestamps) { $data['updated_at'] = date('Y-m-d H:i:s'); } $setParts = []; $params = []; foreach ($data as $column => $value) { $setParts[] = "{$column} = ?"; $params[] = $value; } $sql = "UPDATE {$this->table} SET ".implode(', ', $setParts); $sql .= $this->buildWhereClause(); $params = array_merge($params, $this->getWhereParams()); $affectedRows = $this->db->execute($sql, $params); // Reset query builder $this->resetQuery(); return $affectedRows; } /** * Update record by primary key */ public function updateById($id, array $data): int { return $this->where($this->primaryKey, $id)->update($data); } /** * Delete records matching current query */ public function delete(): int { if (empty($this->wheres)) { throw new Exception("Cannot delete without WHERE conditions for safety"); } $sql = "DELETE FROM {$this->table}"; $sql .= $this->buildWhereClause(); $params = $this->getWhereParams(); $affectedRows = $this->db->execute($sql, $params); // Reset query builder $this->resetQuery(); return $affectedRows; } /** * Delete record by primary key */ public function deleteById($id): int { return $this->where($this->primaryKey, $id)->delete(); } /** * Add WHERE condition */ public function where(string $column, $operator, $value = null): self { // If only 2 parameters, assume operator is '=' if ($value === null) { $value = $operator; $operator = '='; } $this->wheres[] = [ 'type' => 'basic', 'column' => $column, 'operator' => $operator, 'value' => $value, 'boolean' => 'AND' ]; return $this; } /** * Add OR WHERE condition */ public function orWhere(string $column, $operator, $value = null): self { if ($value === null) { $value = $operator; $operator = '='; } $this->wheres[] = [ 'type' => 'basic', 'column' => $column, 'operator' => $operator, 'value' => $value, 'boolean' => 'OR' ]; return $this; } /** * Add WHERE IN condition */ public function whereIn(string $column, array $values): self { $this->wheres[] = [ 'type' => 'in', 'column' => $column, 'values' => $values, 'boolean' => 'AND' ]; return $this; } /** * Add WHERE NOT IN condition */ public function whereNotIn(string $column, array $values): self { $this->wheres[] = [ 'type' => 'not_in', 'column' => $column, 'values' => $values, 'boolean' => 'AND' ]; return $this; } /** * Add WHERE NULL condition */ public function whereNull(string $column): self { $this->wheres[] = [ 'type' => 'null', 'column' => $column, 'boolean' => 'AND' ]; return $this; } /** * Add WHERE NOT NULL condition */ public function whereNotNull(string $column): self { $this->wheres[] = [ 'type' => 'not_null', 'column' => $column, 'boolean' => 'AND' ]; return $this; } /** * Add WHERE LIKE condition */ public function whereLike(string $column, string $value): self { return $this->where($column, 'LIKE', $value); } /** * Add ORDER BY clause */ public function orderBy(string $column, string $direction = 'ASC'): self { $this->orderBy[] = [ 'column' => $column, 'direction' => strtoupper($direction) ]; return $this; } /** * Add LIMIT clause */ public function limit(int $limit): self { $this->limit = $limit; return $this; } /** * Add OFFSET clause */ public function offset(int $offset): self { $this->offset = $offset; return $this; } /** * Set SELECT columns */ public function select(array $columns): self { $this->select = $columns; return $this; } /** * Add JOIN clause */ public function join(string $table, string $first, string $operator, string $second, string $type = 'INNER'): self { $this->joins[] = [ 'type' => $type, 'table' => $table, 'first' => $first, 'operator' => $operator, 'second' => $second ]; return $this; } /** * Add LEFT JOIN clause */ public function leftJoin(string $table, string $first, string $operator, string $second): self { return $this->join($table, $first, $operator, $second, 'LEFT'); } /** * Add RIGHT JOIN clause */ public function rightJoin(string $table, string $first, string $operator, string $second): self { return $this->join($table, $first, $operator, $second, 'RIGHT'); } /** * Paginate results */ public function paginate(int $page = 1, int $perPage = 15): array { $offset = ($page - 1) * $perPage; // Get total count $total = $this->count(); // Get paginated results $results = $this->limit($perPage)->offset($offset)->get(); return [ 'data' => $results, 'current_page' => $page, 'per_page' => $perPage, 'total' => $total, 'last_page' => ceil($total / $perPage), 'from' => $offset + 1, 'to' => min($offset + $perPage, $total) ]; } /** * Build SELECT query */ protected function buildSelectQuery(): string { $sql = "SELECT ".implode(', ', $this->select)." FROM {$this->table}"; // Add JOINs foreach ($this->joins as $join) { $sql .= " {$join['type']} JOIN {$join['table']} ON {$join['first']} {$join['operator']} {$join['second']}"; } // Add WHERE clause $sql .= $this->buildWhereClause(); // Add ORDER BY if (!empty($this->orderBy)) { $orderParts = []; foreach ($this->orderBy as $order) { $orderParts[] = "{$order['column']} {$order['direction']}"; } $sql .= " ORDER BY ".implode(', ', $orderParts); } // Add LIMIT and OFFSET if ($this->limit !== null) { $sql .= " LIMIT {$this->limit}"; } if ($this->offset !== null) { $sql .= " OFFSET {$this->offset}"; } return $sql; } /** * Build WHERE clause */ protected function buildWhereClause(): string { if (empty($this->wheres)) { return ''; } $sql = ' WHERE '; $parts = []; foreach ($this->wheres as $i => $where) { $part = ''; // Add boolean operator (except for first condition) if ($i > 0) { $part .= " {$where['boolean']} "; } switch ($where['type']) { case 'basic': $part .= "{$where['column']} {$where['operator']} ?"; break; case 'in': $placeholders = str_repeat('?,', count($where['values']) - 1).'?'; $part .= "{$where['column']} IN ({$placeholders})"; break; case 'not_in': $placeholders = str_repeat('?,', count($where['values']) - 1).'?'; $part .= "{$where['column']} NOT IN ({$placeholders})"; break; case 'null': $part .= "{$where['column']} IS NULL"; break; case 'not_null': $part .= "{$where['column']} IS NOT NULL"; break; } $parts[] = $part; } return $sql.implode('', $parts); } /** * Get parameters for WHERE clause */ protected function getWhereParams(): array { $params = []; foreach ($this->wheres as $where) { switch ($where['type']) { case 'basic': $params[] = $where['value']; break; case 'in': case 'not_in': $params = array_merge($params, $where['values']); break; } } return $params; } /** * Filter data to only fillable attributes */ protected function filterFillable(array $data): array { if (empty($this->fillable)) { // If no fillable defined, exclude guarded return array_diff_key($data, array_flip($this->guarded)); } // Only include fillable attributes return array_intersect_key($data, array_flip($this->fillable)); } /** * Cast attributes to appropriate types */ protected function castAttributes(array $attributes): array { foreach ($this->casts as $key => $type) { if (array_key_exists($key, $attributes)) { $attributes[$key] = $this->castAttribute($attributes[$key], $type); } } // Hide hidden attributes return array_diff_key($attributes, array_flip($this->hidden)); } /** * Cast single attribute */ protected function castAttribute($value, string $type) { if ($value === null) { return null; } switch ($type) { case 'int': case 'integer': return (int) $value; case 'float': case 'double': return (float) $value; case 'bool': case 'boolean': return (bool) $value; case 'string': return (string) $value; case 'array': case 'json': return is_string($value) ? json_decode($value, true) : $value; case 'object': return is_string($value) ? json_decode($value) : $value; case 'datetime': return new \DateTime($value); default: return $value; } } /** * Reset query builder */ protected function resetQuery(): void { $this->wheres = []; $this->joins = []; $this->orderBy = []; $this->limit = null; $this->offset = null; $this->select = ['*']; } /** * Execute raw SQL query */ public function raw(string $sql, array $params = []): array { return $this->db->query($sql, $params); } /** * Begin database transaction */ public function beginTransaction(): bool { return $this->db->beginTransaction(); } /** * Commit database transaction */ public function commit(): bool { return $this->db->commit(); } /** * Rollback database transaction */ public function rollback(): bool { return $this->db->rollback(); } /** * Execute callback within transaction */ public function transaction(callable $callback) { return $this->db->transaction($callback); } }