['*'], 'where' => '', 'from' => '', 'leftJoin' => '', 'rightJoin' => '', 'innerJoin' => '', 'AndOr' => '', 'orderBy' => '', 'groupBy' => '', 'limit' => '' ]; /** * Sirve para obtener la instancia de la base de datos. * * @return PDO */ protected static function db() : PDO { if (is_null(static::$db)) static::$db = Database::getInstance(); return static::$db; } /** * Ejecuta PDO::beginTransaction para iniciar una transacción. * Más info: https://www.php.net/manual/es/pdo.begintransaction.php * * @return bool */ public 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 function rollBack() : bool { return static::db()->rollBack(); } /** * Ejecuta PDO::commit para consignar una transacción. * Más info: https://www.php.net/manual/es/pdo.commit.php * * @return bool */ public function commit() : bool { return static::db()->commit(); } /** * 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); echo "
"; 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. */ protected static function resetQuery() { static::$querySelect = [ 'select' => ['*'], 'where' => '', 'from' => '', 'leftJoin' => '', 'rightJoin' => '', 'innerJoin' => '', 'AndOr' => '', '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['AndOr'] != '') { $sql .= static::$querySelect['AndOr']; } } 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; } /** * 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 Model * Retorna un objeto de la clase actual. */ protected static function getInstance(array $elem = []) : Model { $class = get_called_class(); $instance = new $class; foreach ($elem as $key => $value) { $instance->$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) $result[$property->name] = isset($this->{$property->name}) ? $this->{$property->name} : null; foreach (static::$ignoreSave as $del) unset($result[$del]); 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'; return $result; } /** * Devuelve el nombre de la clase actual aunque sea una clase extendida. * * @return string * Devuelve el nombre de la clase actual. */ public static function className() : string { return strtolower(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::className().static::$tableSufix; } /** * Actualiza los valores en la BD con los valores del objeto actual. */ protected function update() { $atts = $this->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. */ protected function add() { $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. */ public function save() { $pk = static::$primaryKey; if (isset($this->$pk)) $this->update(); else $this->add(); } /** * Elimina el objeto actual de la base de datos. */ public function delete() { $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 Model */ public static function select(array $columns) : Model { 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 Model */ public static function from(array $tables) : Model { 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 $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 Model */ public static function where(string $column, string $operatorOrValue, string $value=null, bool $no_filter = false) : Model { if (is_null($value)) { $value = $operatorOrValue; $operatorOrValue = '='; } if ($no_filter) { static::$querySelect['where'] = "$column$operatorOrValue$value"; } else { static::$queryVars[":where_$column"] = $value; static::$querySelect['where'] = "$column$operatorOrValue:where_$column"; } 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 Model */ public static function where_in(string $column, array $arr, bool $in = true) : Model { $arrIn = []; foreach($arr as $index => $value) { $arrIn[] = ":on_$index"; static::$queryVars[":on_$index"] = $value; } if ($in) static::$querySelect['where'] = "$column IN (".join(', ', $arrIn).")"; else static::$querySelect['where'] = "$column NOT IN (".join(', ', $arrIn).")"; 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 $columnB * (opcional) Columna a comparar para hacer el join. * * @return Model */ public static function leftJoin(string $table, string $columnA, string $operatorOrColumnB, string $columnB = null) : Model { 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 $columnB * (opcional) Columna a comparar para hacer el join. * * @return Model */ public static function rightJoin(string $table, string $columnA, string $operatorOrColumnB, string $columnB = null) : Model { 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 $columnB * (opcional) Columna a comparar para hacer el join. * * @return Model */ public static function innerJoin(string $table, string $columnA, string $operatorOrColumnB, string $columnB = null) : Model { if (is_null($columnB)) { $columnB = $operatorOrColumnB; $operatorOrColumnB = '='; } static::$querySelect['innerJoin'] .= ' INNER JOIN ' . $table . ' ON ' . "$columnA$operatorOrColumnB$columnB"; return new static(); } /** * 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 $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 Model */ public static function and(string $column, string $operatorOrValue, string $value=null, bool $no_filter = false) : Model { if (is_null($value)) { $value = $operatorOrValue; $operatorOrValue = '='; } if ($no_filter) static::$querySelect['AndOr'] .= " AND $column$operatorOrValue$value"; else { static::$queryVars[":and_$column"] = $value; static::$querySelect['AndOr'] .= " AND $column$operatorOrValue:and_$column"; } 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 $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 Model */ public static function or(string $column, string $operatorOrValue, string $value=null, bool $no_filter = false) : Model { if (is_null($value)) { $value = $operatorOrValue; $operatorOrValue = '='; } if ($no_filter) static::$querySelect['AndOr'] .= " OR $column$operatorOrValue$value"; else { static::$queryVars[":or_$column"] = $value; static::$querySelect['AndOr'] .= " OR $column$operatorOrValue:or_$column"; } return new static(); } /** * Define GROUP BY en la sentencia SQL. * * @param array $arr * Columnas por las que se agrupará. * * @return Model */ public static function groupBy(array $arr) : Model { 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 Model */ public static function limit(int $offsetOrQuantity, ?int $quantity = null) : Model { 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 Model */ public static function orderBy(string $value, string $order = 'ASC') : Model { 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::$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 mixed */ public static function getById($id) { 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 $in * (opcional) Columnas en las que se va a buscar (null para buscar en todas). * * @return Model */ public static function search(string $search, array $in = null) : Model { if ($in == null) { $className = get_called_class(); $in = array_keys((new $className())->getVars()); } $db = static::db(); static::$queryVars[':search'] = $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 * Contiene un arreglo de instancias de la clase actual. */ public static function get(bool $resetQuery = true) : array { // Devuelve array vacío si no encuentra nada. $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 mixed * Puede retornar un objeto Model o null. */ public static function getFirst(bool $resetQuery = true) { // Devuelve null si no encuentra nada. 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 $atts */ public function setNull(array $atts) { foreach ($atts as $att) { if (!in_array($att, $this->toNull)) $this->toNull[] = $att; } } } ?>