['*'], '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 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 { 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 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); 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.
     * @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_column($properties, 'name');

        foreach ($elem as $key => $value) {
            $index = array_search($key, $propertyNames);
            if (is_numeric($index) && enum_exists($properties[$index]->getType()->getName()))
                $instance->$key = $properties[$index]->getType()->getName()::tryfrom($value);
            else
                $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';

            if ($property instanceof \UnitEnum)
                $result[$i] = $property->value;
        }

        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(
            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(array $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(array $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 $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 $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 $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 where_in(
        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 $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 $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 $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 $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 string|array $atts
     *   Atributo o arreglo de atributos que se definirán como nulos.
     *
     * @return void
     */
    public function setNull(string|array $atts): void
    {
        if (is_array($atts)) {
            foreach ($atts as $att)
                if (!in_array($att, $this->toNull))
                    $this->toNull[] = $att;
            return;
        }

        if (!in_array($atts, $this->toNull))
            $this->toNull[] = $atts;
    }
}
?>