['*'], 'where' => '', 'from' => '', 'leftJoin' => '', 'rightJoin' => '', 'innerJoin' => '', 'orderBy' => '', 'groupBy' => '', 'limit' => '', ]; /** * Sirve para obtener la instancia de la base de datos. * * @return PDO */ protected static function db(): PDO { if (DB_TYPE == 'sqlite') { return Database::getInstance( type: DB_TYPE, name: DB_NAME ); } else { return Database::getInstance( DB_TYPE, DB_HOST, DB_NAME, DB_USER, DB_PASS ); } } /** * Ejecuta PDO::beginTransaction para iniciar una transacción. * Más info: https://www.php.net/manual/es/pdo.begintransaction.php * * @return bool */ public static function beginTransaction(): bool { return static::db()->beginTransaction(); } /** * Ejecuta PDO::rollBack para deshacher los cambios de una transacción. * Más info: https://www.php.net/manual/es/pdo.rollback.php * * @return bool */ public static function rollBack(): bool { if (static::db()->inTransaction()) { return static::db()->rollBack(); } else { return true; } } /** * Ejecuta PDO::commit para consignar una transacción. * Más info: https://www.php.net/manual/es/pdo.commit.php * * @return bool */ public static function commit(): bool { if (static::db()->inTransaction()) { return static::db()->commit(); } else { return true; } } /** * Ejecuta una sentencia SQL en la base de datos. * * @param string $query * Contiene la sentencia SQL que se desea ejecutar. * * @throws Exception * En caso de que la sentencia SQL falle, devolverá un error en * pantalla y hará rolllback en caso de estar dentro de una * transacción (ver método beginTransacction). * * @param bool $resetQuery * Indica si el query debe reiniciarse o no (por defecto es true). * * @return array * Contiene el resultado de la llamada SQL . */ protected static function query(string $query, bool $resetQuery = true): array { $db = static::db(); try { $prepared = $db->prepare($query); $prepared->execute(static::$queryVars); } catch (PDOException $e) { if ($db->inTransaction()) { $db->rollBack(); } $vars = json_encode(static::$queryVars); throw new Exception( "\nError at query to database.\n" . "Query: $query\n" . "Vars: $vars\n" . "Error:\n" . $e->getMessage() ); } $result = $prepared->fetchAll(); if ($resetQuery) { static::resetQuery(); } return $result; } /** * Reinicia la configuración de la sentencia SQL. * @return void */ protected static function resetQuery(): void { static::$querySelect = [ 'select' => ['*'], 'where' => '', 'from' => '', 'leftJoin' => '', 'rightJoin' => '', 'innerJoin' => '', 'orderBy' => '', 'groupBy' => '', 'limit' => '', ]; static::$queryVars = []; } /** * Construye la sentencia SQL a partir static::$querySelect y una vez * construída, llama a resetQuery. * * @return string * Contiene la sentencia SQL. */ protected static function buildQuery(): string { $sql = 'SELECT ' . join(', ', static::$querySelect['select']); if (static::$querySelect['from'] != '') { $sql .= ' FROM ' . static::$querySelect['from']; } else { $sql .= ' FROM ' . static::table(); } if (static::$querySelect['innerJoin'] != '') { $sql .= static::$querySelect['innerJoin']; } if (static::$querySelect['leftJoin'] != '') { $sql .= static::$querySelect['leftJoin']; } if (static::$querySelect['rightJoin'] != '') { $sql .= static::$querySelect['rightJoin']; } if (static::$querySelect['where'] != '') { $sql .= ' WHERE ' . static::$querySelect['where']; } if (static::$querySelect['groupBy'] != '') { $sql .= ' GROUP BY ' . static::$querySelect['groupBy']; } if (static::$querySelect['orderBy'] != '') { $sql .= ' ORDER BY ' . static::$querySelect['orderBy']; } if (static::$querySelect['limit'] != '') { $sql .= ' LIMIT ' . static::$querySelect['limit']; } return $sql; } /** * Configura $queryVars para vincular un valor a un * parámetro de sustitución y devuelve este último. * * @param string $value * Valor a vincular. * * @return string * Parámetro de sustitución. */ private static function bindValue(string $value): string { $index = ':v_' . count(static::$queryVars); static::$queryVars[$index] = $value; return $index; } /** * Crea una instancia del objeto actual a partir de un arreglo. * * @param mixed $elem * Puede recibir un arreglo o un objeto que contiene los valores * que tendrán sus atributos. * * @return static * Retorna un objeto de la clase actual. */ protected static function getInstance(array $elem = []): static { $class = get_called_class(); $instance = new $class(); $reflection = new ReflectionClass($instance); $properties = $reflection->getProperties(); $propertyNames = array_map(function ($property) { return static::camelCaseToSnakeCase($property->name); }, $properties); foreach ($elem as $key => $value) { $index = array_search($key, $propertyNames); if (is_numeric($index)) { if (enum_exists($properties[$index]->getType()->getName())) { $instance->{$properties[$index]->name} = $properties[$index]->getType()->getName()::tryfrom($value); } else { $instance->{$properties[$index]->name} = $value; } } else { $instance->{static::snakeCaseToCamelCase($key)} = $value; } } return $instance; } /** * Devuelve los atributos a guardar de la case actual. * Los atributos serán aquellos que seran public y * no esten excluidos en static::$ignoresave y aquellos * que sean private o protected pero estén en static::$forceSave. * * @return array * Contiene los atributos indexados del objeto actual. */ protected function getVars(): array { $reflection = new ReflectionClass($this); $properties = $reflection->getProperties(ReflectionProperty::IS_PUBLIC); $result = []; foreach ($properties as $property) { if (!in_array($property->name, static::$ignoreSave)) { $result[$this->camelCaseToSnakeCase($property->name)] = isset($this->{$property->name}) ? $this->{$property->name} : null; } } foreach (static::$forceSave as $value) { $result[$value] = isset($this->$value) ? $this->$value : null; } foreach ($result as $i => $property) { if (gettype($property) == 'boolean') { $result[$i] = $property ? '1' : '0'; } if ($property instanceof \UnitEnum) { $result[$i] = $property->value ?? $property->name; } } return $result; } /** * Devuelve el nombre de la clase actual aunque sea una clase extendida. * * @return string * */ public static function className(): string { return substr( strrchr(get_called_class(), '\\'), 1 ); } /** * Construye (a partir del nombre de la clase y el sufijo en static::$tableSufix) * y/o develve el nombre de la tabla de la BD en la que se alojará o * se aloja el objeto actual. * * @return string */ protected static function table(): string { if (isset(static::$table)) { return static::$table; } return static::camelCaseToSnakeCase(static::className()) . 's'; } /** * Convierte de lowerCamelCase a snake_case * * @param string $string * * @return string */ protected static function camelCaseToSnakeCase(string $string): string { return strtolower( preg_replace( '/(?getVars(); foreach ($atts as $key => $value) { if (isset($value)) { if (in_array($key, $this->toNull)) { $set[] = "$key=NULL"; } else { $set[] = "$key=:$key"; static::$queryVars[':' . $key] = $value; } } else { if (in_array($key, $this->toNull)) { $set[] = "$key=NULL"; } } } $table = static::table(); $pk = static::$primaryKey; $pkv = $this->$pk; $sql = "UPDATE $table SET " . join(', ', $set) . " WHERE $pk='$pkv'"; static::query($sql); } /** * Inserta una nueva fila en la base de datos a partir del * objeto actual. * @return void */ protected function add(): void { $db = static::db(); $atts = $this->getVars(); foreach ($atts as $key => $value) { if (isset($value)) { $into[] = "`$key`"; $values[] = ":$key"; static::$queryVars[":$key"] = $value; } } $table = static::table(); $sql = "INSERT INTO $table (" . join(', ', $into) . ") VALUES (" . join(', ', $values) . ")"; static::query($sql); $pk = static::$primaryKey; $this->$pk = $db->lastInsertId(); } /** * Revisa si el objeto a guardar es nuevo o no y según el resultado * llama a update para actualizar o add para insertar una nueva fila. * @return void */ public function save(): void { $pk = static::$primaryKey; if (isset($this->$pk)) { $this->update(); } else { $this->add(); } } /** * Elimina el objeto actual de la base de datos. * @return void */ public function delete(): void { $table = static::table(); $pk = static::$primaryKey; $sql = "DELETE FROM $table WHERE $pk=:$pk"; static::$queryVars[":$pk"] = $this->$pk; static::query($sql); } /** * Define SELECT en la sentencia SQL. * * @param array $columns * Columnas que se selecionarán en la consulta SQL. * * @return static */ public static function select(...$columns): static { static::$querySelect['select'] = $columns; return new static(); } /** * Define FROM en la sentencia SQL. * * @param array $tables * Tablas que se selecionarán en la consulta SQL. * * @return static */ public static function from(...$tables): static { static::$querySelect['from'] = join(', ', $tables); return new static(); } /** * Define el WHERE en la sentencia SQL. * * @param string $column * La columna a comparar. * * @param string $operatorOrValue * El operador o el valor a comparar como igual en caso de que $value no se defina. * * @param string|null $value * (opcional) El valor a comparar en la columna. * * @param bool $no_filter * (opcional) Se usa cuando $value es una columna o un valor que no requiere filtros * contra ataques SQLI (por defeco es false). * * @return static */ public static function where( string $column, string $operatorOrValue, ?string $value = null, bool $no_filter = false ): static { return static::and( $column, $operatorOrValue, $value, $no_filter ); } /** * Define AND en la sentencia SQL (se puede anidar). * * @param string $column * La columna a comparar. * * @param string $operatorOrValue * El operador o el valor a comparar como igual en caso de que $value no se defina. * * @param string|null $value * (opcional) El valor el valor a comparar en la columna. * * @param bool $no_filter * (opcional) Se usa cuando $value es una columna o un valor que no requiere filtros * contra ataques SQLI (por defecto es false). * * @return static */ public static function and( string $column, string $operatorOrValue, ?string $value = null, bool $no_filter = false ): static { if (is_null($value)) { $value = $operatorOrValue; $operatorOrValue = '='; } if (!$no_filter) { $value = static::bindValue($value); } if (static::$querySelect['where'] == '') { static::$querySelect['where'] = "$column $operatorOrValue $value"; } else { static::$querySelect['where'] .= " AND $column $operatorOrValue $value"; } return new static(); } /** * Define OR en la sentencia SQL (se puede anidar). * * @param string $column * La columna a comparar. * * @param string $operatorOrValue * El operador o el valor a comparar como igual en caso de que $value no se defina. * * @param string|null $value * (opcional) El valor el valor a comparar en la columna. * * @param bool $no_filter * (opcional) Se usa cuando $value es una columna o un valor que no requiere filtros * contra ataques SQLI (por defecto es false). * * @return static */ public static function or( string $column, string $operatorOrValue, ?string $value = null, bool $no_filter = false ): static { if (is_null($value)) { $value = $operatorOrValue; $operatorOrValue = '='; } if (!$no_filter) { $value = static::bindValue($value); } if (static::$querySelect['where'] == '') { static::$querySelect['where'] = "$column $operatorOrValue $value"; } else { static::$querySelect['where'] .= " OR $column $operatorOrValue $value"; } return new static(); } /** * Define WHERE usando IN en la sentencia SQL. * * @param string $column * La columna a comparar. * * @param array $arr * Arreglo con todos los valores a comparar con la columna. * * @param bool $in * Define si se tienen que comprobar negativa o positivamente. * * @return static */ public static function whereIn( string $column, array $arr, bool $in = true ): static { $arrIn = []; foreach ($arr as $value) { $arrIn[] = static::bindValue($value); } if ($in) { $where_in = "$column IN (" . join(', ', $arrIn) . ")"; } else { $where_in = "$column NOT IN (" . join(', ', $arrIn) . ")"; } if (static::$querySelect['where'] == '') { static::$querySelect['where'] = $where_in; } else { static::$querySelect['where'] .= " AND $where_in"; } return new static(); } /** * Define LEFT JOIN en la sentencia SQL. * * @param string $table * Tabla que se va a juntar a la del objeto actual. * * @param string $columnA * Columna a comparar para hacer el join. * * @param string $operatorOrColumnB * Operador o columna a comparar como igual para hacer * el join en caso de que $columnB no se defina. * * @param string|null $columnB * (opcional) Columna a comparar para hacer el join. * * @return static */ public static function leftJoin( string $table, string $columnA, string $operatorOrColumnB, ?string $columnB = null ): static { if (is_null($columnB)) { $columnB = $operatorOrColumnB; $operatorOrColumnB = '='; } static::$querySelect['leftJoin'] .= ' LEFT JOIN ' . $table . ' ON ' . "$columnA$operatorOrColumnB$columnB"; return new static(); } /** * Define RIGHT JOIN en la sentencia SQL. * * @param string $table * Tabla que se va a juntar a la del objeto actual. * * @param string $columnA * Columna a comparar para hacer el join. * * @param string $operatorOrColumnB * Operador o columna a comparar como igual para hacer * el join en caso de que $columnB no se defina. * * @param string|null $columnB * (opcional) Columna a comparar para hacer el join. * * @return static */ public static function rightJoin( string $table, string $columnA, string $operatorOrColumnB, ?string $columnB = null ): static { if (is_null($columnB)) { $columnB = $operatorOrColumnB; $operatorOrColumnB = '='; } static::$querySelect['rightJoin'] .= ' RIGHT JOIN ' . $table . ' ON ' . "$columnA$operatorOrColumnB$columnB"; return new static(); } /** * Define INNER JOIN en la sentencia SQL. * * @param string $table * Tabla que se va a juntar a la del objeto actual. * * @param string $columnA * Columna a comparar para hacer el join. * * @param string $operatorOrColumnB * Operador o columna a comparar como igual para hacer * el join en caso de que $columnB no se defina. * * @param string|null $columnB * (opcional) Columna a comparar para hacer el join. * * @return static */ public static function innerJoin( string $table, string $columnA, string $operatorOrColumnB, ?string $columnB = null ): static { if (is_null($columnB)) { $columnB = $operatorOrColumnB; $operatorOrColumnB = '='; } static::$querySelect['innerJoin'] .= ' INNER JOIN ' . $table . ' ON ' . "$columnA$operatorOrColumnB$columnB"; return new static(); } /** * Define GROUP BY en la sentencia SQL. * * @param array $arr * Columnas por las que se agrupará. * * @return static */ public static function groupBy(array $arr): static { static::$querySelect['groupBy'] = join(', ', $arr); return new static(); } /** * Define LIMIT en la sentencia SQL. * * @param int $offsetOrQuantity * Define el las filas a ignorar o la cantidad a tomar en * caso de que $quantity no esté definido. * @param int $quantity * Define la cantidad máxima de filas a tomar. * * @return static */ public static function limit(int $offsetOrQuantity, ?int $quantity = null): static { if (is_null($quantity)) { static::$querySelect['limit'] = $offsetOrQuantity; } else { static::$querySelect['limit'] = $offsetOrQuantity . ', ' . $quantity; } return new static(); } /** * Define ORDER BY en la sentencia SQL. * * @param string $value * Columna por la que se ordenará. * * @param string $order * (opcional) Define si el orden será de manera ascendente (ASC), * descendente (DESC) o aleatorio (RAND). * * @return static */ public static function orderBy(string $value, string $order = 'ASC'): static { if ($value == "RAND") { static::$querySelect['orderBy'] = 'RAND()'; return new static(); } if (!(strtoupper($order) == 'ASC' || strtoupper($order) == 'DESC')) { $order = 'ASC'; } static::$querySelect['orderBy'] = $value . ' ' . $order; return new static(); } /** * Retorna la cantidad de filas que hay en un query. * * @param bool $resetQuery * (opcional) Indica si el query debe reiniciarse o no (por defecto es true). * * @param bool $useLimit * (opcional) Permite usar limit para estabecer un máximo inical y final para contar. * Requiere que se haya definido antes el límite (por defecto en false). * * @return int */ public static function count(bool $resetQuery = true, bool $useLimit = false): int { if (!$resetQuery) { $backup = [ 'select' => static::$querySelect['select'], 'limit' => static::$querySelect['limit'], 'orderBy' => static::$querySelect['orderBy'], ]; } if ($useLimit && static::$querySelect['limit'] != '') { static::$querySelect['select'] = ['1']; static::$querySelect['orderBy'] = ''; $sql = 'SELECT COUNT(1) AS quantity FROM (' . static::buildQuery() . ') AS counted'; $queryResult = static::query($sql, $resetQuery); $result = $queryResult[0]['quantity']; } else { static::$querySelect['select'] = [ "COUNT(" . static::table() . "." . static::$primaryKey . ") as quantity", ]; static::$querySelect['limit'] = '1'; static::$querySelect['orderBy'] = ''; $sql = static::buildQuery(); $queryResult = static::query($sql, $resetQuery); $result = $queryResult[0]['quantity']; } if (!$resetQuery) { static::$querySelect['select'] = $backup['select']; static::$querySelect['limit'] = $backup['limit']; static::$querySelect['orderBy'] = $backup['orderBy']; } return $result; } /** * Obtiene una instancia según su primary key (generalmente id). * Si no encuentra una instancia, devuelve nulo. * * @param mixed $id * * @return static|null */ public static function getById(mixed $id): ?static { return static::where(static::$primaryKey, $id)->getFirst(); } /** * Realiza una búsqueda en la tabla de la instancia actual. * * @param string $search * Contenido a buscar. * * @param array|null $in * (opcional) Columnas en las que se va a buscar (null para buscar en todas). * * @return static */ public static function search(string $search, ?array $in = null): static { if ($in == null) { $className = get_called_class(); $in = array_keys((new $className())->getVars()); } $search = static::bindValue($search); $where = []; if (DB_TYPE == 'sqlite') { foreach ($in as $row) { $where[] = "$row LIKE '%' || $search || '%'"; } } else { foreach ($in as $row) { $where[] = "$row LIKE CONCAT('%', $search, '%')"; } } if (static::$querySelect['where'] == '') { static::$querySelect['where'] = join(' OR ', $where); } else { static::$querySelect['where'] = static::$querySelect['where'] . ' AND (' . join(' OR ', $where) . ')'; } return new static(); } /** * Obtener los resultados de la consulta SQL. * * @param bool $resetQuery * (opcional) Indica si el query debe reiniciarse o no (por defecto es true). * * @return array * Arreglo con instancias del la clase actual resultantes del query. */ public static function get(bool $resetQuery = true): array { $sql = static::buildQuery(); $result = static::query($sql, $resetQuery); $instances = []; foreach ($result as $row) { $instances[] = static::getInstance($row); } return $instances; } /** * El primer elemento de la consulta SQL. * * @param bool $resetQuery * (opcional) Indica si el query debe reiniciarse o no (por defecto es true). * * @return static|null * Puede retornar una instancia de la clase actual o null. */ public static function getFirst(bool $resetQuery = true): ?static { static::limit(1); $instances = static::get($resetQuery); return empty($instances) ? null : $instances[0]; } /** * Obtener todos los elementos del la tabla de la instancia actual. * * @return array * Contiene un arreglo de instancias de la clase actual. */ public static function all(): array { $sql = 'SELECT * FROM ' . static::table(); $result = static::query($sql); $instances = []; foreach ($result as $row) { $instances[] = static::getInstance($row); } return $instances; } /** * Permite definir como nulo el valor de un atributo. * Sólo funciona para actualizar un elemento de la BD, no para insertar. * * @param array $attributes * Atributo o arreglo de atributos que se definirán como nulos. * * @return void */ public function setNull(string ...$attributes): void { foreach ($attributes as $att) { if (!in_array($att, $this->toNull)) { $this->toNull[] = $att; } } } }