['*'], '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;
        }
    }
}
?>