First commit.
This commit is contained in:
commit
a6b849e36f
|
@ -0,0 +1,16 @@
|
||||||
|
<?php
|
||||||
|
// Configuración de la base de datos
|
||||||
|
define('DB_TYPE', 'mysql');
|
||||||
|
define('DB_HOST', 'localhost');
|
||||||
|
define('DB_NAME', 'pool');
|
||||||
|
define('DB_USER', 'root');
|
||||||
|
define('DB_PASS', '');
|
||||||
|
|
||||||
|
// Configuración del sitio
|
||||||
|
define('SITE_URL', 'https://pool.kj5.top/');
|
||||||
|
|
||||||
|
define('PRIVATE_KEY', 'AerohrejaeLohz2eojai2ba1ohCiegoh');
|
||||||
|
|
||||||
|
// Configuración avanzada
|
||||||
|
define('ROOT_DIR', __DIR__);
|
||||||
|
define('ROOT_CORE', ROOT_DIR.'/src');
|
|
@ -0,0 +1,24 @@
|
||||||
|
CREATE TABLE users (
|
||||||
|
id BIGINT PRIMARY KEY AUTO_INCREMENT,
|
||||||
|
username VARCHAR(255),
|
||||||
|
password VARCHAR(255)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE pools (
|
||||||
|
id BIGINT PRIMARY KEY AUTO_INCREMENT,
|
||||||
|
title VARCHAR(255)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE pool_options (
|
||||||
|
id BIGINT PRIMARY KEY AUTO_INCREMENT,
|
||||||
|
value TEXT,
|
||||||
|
pool_id BIGINT,
|
||||||
|
FOREIGN KEY (pool_id) REFERENCES pools(id)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE answers (
|
||||||
|
id BIGINT PRIMARY KEY AUTO_INCREMENT,
|
||||||
|
option_id BIGINT,
|
||||||
|
create_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
FOREIGN KEY (option_id) REFERENCES pool_options(id)
|
||||||
|
);
|
|
@ -0,0 +1,24 @@
|
||||||
|
<?php
|
||||||
|
require_once('../config.php');
|
||||||
|
|
||||||
|
// Incluir clases
|
||||||
|
spl_autoload_register(function ($className) {
|
||||||
|
$fp = str_replace('\\','/',$className);
|
||||||
|
$name = basename($fp);
|
||||||
|
$dir = dirname($fp);
|
||||||
|
$file = ROOT_CORE.'/'.$dir.'/'.$name.'.php';
|
||||||
|
if (file_exists($file)) {
|
||||||
|
require_once $file;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Incluir routers
|
||||||
|
$routers = glob(ROOT_CORE.'/Routers/*.php');
|
||||||
|
|
||||||
|
foreach($routers as $file){
|
||||||
|
require_once($file);
|
||||||
|
}
|
||||||
|
|
||||||
|
\Libs\Router::apply();
|
||||||
|
?>
|
File diff suppressed because one or more lines are too long
|
@ -0,0 +1,44 @@
|
||||||
|
.centered-container {
|
||||||
|
display: grid;
|
||||||
|
min-height: 100vh;
|
||||||
|
place-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.center {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pool-options {
|
||||||
|
display: grid;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.poll-wrapper {
|
||||||
|
display: grid;
|
||||||
|
min-height: 100vh;
|
||||||
|
align-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.autohide {
|
||||||
|
animation-name: disapear;
|
||||||
|
animation-duration: 4000ms;
|
||||||
|
animation-fill-mode: forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes disapear{
|
||||||
|
0%{
|
||||||
|
opacity: 1;
|
||||||
|
transform: rotateX(90deg);
|
||||||
|
}
|
||||||
|
50%{
|
||||||
|
opacity: 0.5;
|
||||||
|
transform: rotateX(0deg);
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
100%{
|
||||||
|
display: none;
|
||||||
|
opacity: 0;
|
||||||
|
height: 0px;
|
||||||
|
transform: rotateX(90deg);
|
||||||
|
}
|
||||||
|
}
|
File diff suppressed because one or more lines are too long
|
@ -0,0 +1,46 @@
|
||||||
|
#+TITLE: Sistema de encuestas simple
|
||||||
|
#+AUTHOR: KJ
|
||||||
|
|
||||||
|
Un sistema de encuestas con solo 3 opciones a elegir (por si acaso, es posible añadir algunas más, pero se harcodeará esa parte).
|
||||||
|
|
||||||
|
* Especificaciones
|
||||||
|
|
||||||
|
- Encuestas con 3 opciones.
|
||||||
|
- En otra página ira viendo el resultado.
|
||||||
|
- Cualquier dispositivo debe ser posible.
|
||||||
|
- Las opciones se deben poder cambiarse cada 24 horas (iniciar nueva encuesta).
|
||||||
|
|
||||||
|
|
||||||
|
* Diseño de la BD
|
||||||
|
|
||||||
|
** Tabla de usuarios
|
||||||
|
|
||||||
|
| users |
|
||||||
|
|----------|
|
||||||
|
| id |
|
||||||
|
| email |
|
||||||
|
| username |
|
||||||
|
| password |
|
||||||
|
|
||||||
|
** Encuestas (En el hardcode solo se trabajará con la última creada)
|
||||||
|
|
||||||
|
| pools |
|
||||||
|
|-------|
|
||||||
|
| id |
|
||||||
|
| title |
|
||||||
|
|
||||||
|
** Opciones elegibles como respuesta a la encuesta.
|
||||||
|
|
||||||
|
| pool_options |
|
||||||
|
|--------------|
|
||||||
|
| id |
|
||||||
|
| value |
|
||||||
|
| pool_id |
|
||||||
|
|
||||||
|
** Votos de los clientes
|
||||||
|
|
||||||
|
| anwers |
|
||||||
|
|-----------|
|
||||||
|
| id |
|
||||||
|
| option_id |
|
||||||
|
| create_at |
|
|
@ -0,0 +1,19 @@
|
||||||
|
<?php
|
||||||
|
namespace Controllers\Pool;
|
||||||
|
|
||||||
|
use Libs\HTMX;
|
||||||
|
use Requests\PoolRequest;
|
||||||
|
|
||||||
|
class HomeController {
|
||||||
|
/**
|
||||||
|
* handle
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public static function handle(PoolRequest $request): void
|
||||||
|
{
|
||||||
|
HTMX::render('Pool', [
|
||||||
|
'pool' => $request->pool
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,27 @@
|
||||||
|
<?php
|
||||||
|
namespace Controllers\Pool;
|
||||||
|
|
||||||
|
use Models\Pool;
|
||||||
|
use Models\PoolOption;
|
||||||
|
use Requests\PoolCreateRequest;
|
||||||
|
|
||||||
|
class PoolCreateController {
|
||||||
|
public static function handle(PoolCreateRequest $request): void
|
||||||
|
{
|
||||||
|
$pool = new Pool;
|
||||||
|
$pool->title = $request->post->title;
|
||||||
|
$pool->beginTransaction();
|
||||||
|
$pool->save();
|
||||||
|
|
||||||
|
for($i=1; $i <= 3; $i++) {
|
||||||
|
$option = new PoolOption;
|
||||||
|
$option->value = $request->post->{'option'.$i};
|
||||||
|
$option->pool_id = $pool->id;
|
||||||
|
$option->save();
|
||||||
|
}
|
||||||
|
$pool->commit();
|
||||||
|
|
||||||
|
$request->saved = true;
|
||||||
|
PoolFormController::handle($request);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,13 @@
|
||||||
|
<?php
|
||||||
|
namespace Controllers\Pool;
|
||||||
|
|
||||||
|
use Libs\HTMX;
|
||||||
|
use Requests\UserRequest;
|
||||||
|
|
||||||
|
class PoolFormController {
|
||||||
|
public static function handle(UserRequest $request) {
|
||||||
|
HTMX::render('PoolConfig', [
|
||||||
|
'saved' => $request->saved ?? false
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,14 @@
|
||||||
|
<?php
|
||||||
|
namespace Controllers\Pool;
|
||||||
|
|
||||||
|
use Libs\HTMX;
|
||||||
|
use Requests\PoolRequest;
|
||||||
|
|
||||||
|
class PoolResultController {
|
||||||
|
public static function handle(PoolRequest $request): void
|
||||||
|
{
|
||||||
|
HTMX::render('Result', [
|
||||||
|
'pool' => $request->pool
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,19 @@
|
||||||
|
<?php
|
||||||
|
namespace Controllers\Pool;
|
||||||
|
|
||||||
|
use Models\Answer;
|
||||||
|
use Requests\VoteRequest;
|
||||||
|
|
||||||
|
class PoolVoteController {
|
||||||
|
public static function handle(VoteRequest $request): void
|
||||||
|
{
|
||||||
|
if (!$request->pool->isVoted()) {
|
||||||
|
$vote = new Answer;
|
||||||
|
$vote->option_id = $request->option->id;
|
||||||
|
$vote->save();
|
||||||
|
$request->pool->createCookie();
|
||||||
|
}
|
||||||
|
|
||||||
|
echo '¡Gracias por participar!';
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,18 @@
|
||||||
|
<?php
|
||||||
|
namespace Controllers\User;
|
||||||
|
|
||||||
|
use Libs\HTMX;
|
||||||
|
use Models\User;
|
||||||
|
use Requests\LoginRequest;
|
||||||
|
|
||||||
|
class UserLoginController {
|
||||||
|
public static function handle(LoginRequest $request): void
|
||||||
|
{
|
||||||
|
if (!User::login($request->post->username, $request->post->password)) {
|
||||||
|
$request->onInvalid('Usuario o contraseña equivocados.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
HTMX::redirect('/config');
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,21 @@
|
||||||
|
<?php
|
||||||
|
namespace Controllers\User;
|
||||||
|
|
||||||
|
use Libs\HTMX;
|
||||||
|
use Models\User;
|
||||||
|
|
||||||
|
class UserLoginFormController {
|
||||||
|
public static function handle(): void
|
||||||
|
{
|
||||||
|
// Crear usuario en caso de que aún no exista.
|
||||||
|
$user = User::getFirst();
|
||||||
|
if (is_null($user)) {
|
||||||
|
$user = new User;
|
||||||
|
$user->username = 'Paul';
|
||||||
|
$user->setPassword('F1rbpul');
|
||||||
|
$user->save();
|
||||||
|
}
|
||||||
|
|
||||||
|
HTMX::render('Login');
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,111 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Crypto - DuckBrain
|
||||||
|
*
|
||||||
|
* Clase creada para encriptar / desencriptar
|
||||||
|
* usando openssl con el algoritmo AES 256 CBC.
|
||||||
|
*
|
||||||
|
* Depende opcionalmente de la constante PRIVATE_KEY.
|
||||||
|
*
|
||||||
|
* @author KJ
|
||||||
|
* @website https://kj2.me
|
||||||
|
* @licence MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Libs;
|
||||||
|
|
||||||
|
class Crypto {
|
||||||
|
/**
|
||||||
|
* Encripta usando openssl con el algoritmo AES 256 CBC
|
||||||
|
* y devuelve el resultado en hexadecimal.
|
||||||
|
*
|
||||||
|
* @param string $data
|
||||||
|
* @param string $password
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public static function encryptHex(string $data, string $password=PRIVATE_KEY): string
|
||||||
|
{
|
||||||
|
return bin2hex(static::encrypt($data, $password));
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Desencripta un cadena desde hexadecimal
|
||||||
|
* y luego usando openssl con el algoritmo AES 256 CBC.
|
||||||
|
*
|
||||||
|
* @param string $data Cadena en headecimal
|
||||||
|
* @param string $password
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public static function decryptHex(string $data, string $password=PRIVATE_KEY): string
|
||||||
|
{
|
||||||
|
return static::decrypt(hex2bin($data), $password);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encripta usando openssl con el algoritmo AES 256 CBC
|
||||||
|
* y devuelve el resultado en base64.
|
||||||
|
*
|
||||||
|
* @param string $data
|
||||||
|
* @param string $password
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public static function encrypt64(string $data, string $password=PRIVATE_KEY): string
|
||||||
|
{
|
||||||
|
return base64_encode(static::encrypt($data, $password));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Desencripta un cadena desde base64
|
||||||
|
* y luego usando openssl con el algoritmo AES 256 CBC.
|
||||||
|
*
|
||||||
|
* @param string $data
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public static function decrypt64(string $data, string $password=PRIVATE_KEY): string
|
||||||
|
{
|
||||||
|
return static::decrypt(base64_decode($data), $password);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encripta usando openssl con el algoritmo AES 256 CBC.
|
||||||
|
*
|
||||||
|
* @param string $data
|
||||||
|
* @param string $password
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public static function encrypt(string $data, string $password=PRIVATE_KEY): string
|
||||||
|
{
|
||||||
|
return openssl_encrypt(
|
||||||
|
$data,
|
||||||
|
'AES-256-CBC',
|
||||||
|
$password,
|
||||||
|
OPENSSL_RAW_DATA,
|
||||||
|
substr($password, 0, 16)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Desencripta usando openssl con el algoritmo AES 256 CBC.
|
||||||
|
*
|
||||||
|
* @param string $data
|
||||||
|
* @param string $password
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public static function decrypt(string $data, string $password=PRIVATE_KEY) : string
|
||||||
|
{
|
||||||
|
return openssl_decrypt(
|
||||||
|
$data,
|
||||||
|
'AES-256-CBC',
|
||||||
|
$password,
|
||||||
|
OPENSSL_RAW_DATA,
|
||||||
|
substr($password, 0, 16)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,58 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Database - DuckBrain
|
||||||
|
*
|
||||||
|
* Clase diseñada para crear y devolver una única instancia PDO (database).
|
||||||
|
*
|
||||||
|
* @author KJ
|
||||||
|
* @website https://kj2.me
|
||||||
|
* @licence MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Libs;
|
||||||
|
|
||||||
|
use PDO;
|
||||||
|
use PDOException;
|
||||||
|
use Exception;
|
||||||
|
|
||||||
|
class Database extends PDO {
|
||||||
|
static private array $databases = [];
|
||||||
|
|
||||||
|
private function __construct() {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Devuelve una instancia homogénea (singlenton) de la base de datos (PDO).
|
||||||
|
*
|
||||||
|
* @return PDO
|
||||||
|
*/
|
||||||
|
static public function getInstance(
|
||||||
|
string $type = 'mysql',
|
||||||
|
string $host = 'localhost',
|
||||||
|
string $name = '',
|
||||||
|
string $user = '',
|
||||||
|
string $pass = '',
|
||||||
|
): PDO
|
||||||
|
{
|
||||||
|
$key = $type.'/'.$host.'/'.$name.'/'.$user;
|
||||||
|
if (empty(static::$databases[$key])) {
|
||||||
|
|
||||||
|
if ($type == 'sqlite') {
|
||||||
|
$dsn = $type .':'. $name;
|
||||||
|
} else
|
||||||
|
$dsn = $type.':dbname='.$name.';host='.$host;
|
||||||
|
|
||||||
|
try {
|
||||||
|
static::$databases[$key] = new PDO($dsn, $user, $pass);
|
||||||
|
} catch (PDOException $e) {
|
||||||
|
throw new Exception(
|
||||||
|
'Error at connect to database: ' . $e->getMessage()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static::$databases[$key]->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
|
||||||
|
static::$databases[$key]->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC);
|
||||||
|
}
|
||||||
|
return static::$databases[$key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
?>
|
|
@ -0,0 +1,366 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* HTMLComponent - DuckBrain
|
||||||
|
*
|
||||||
|
* Librería de componentes HTML para Duckbrain.
|
||||||
|
*
|
||||||
|
* @author KJ
|
||||||
|
* @website https://kj2.me
|
||||||
|
* @licence MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Libs;
|
||||||
|
|
||||||
|
class HTMLComponent {
|
||||||
|
/**
|
||||||
|
* __construct
|
||||||
|
*
|
||||||
|
* @param array $properties
|
||||||
|
* @param string $viewPath
|
||||||
|
* @param string $extension
|
||||||
|
*/
|
||||||
|
public function __construct(
|
||||||
|
protected array $properties = [],
|
||||||
|
protected string $content = '',
|
||||||
|
protected string $viewPath = ROOT_DIR.'/src/Views/',
|
||||||
|
protected string $extension = '.php',
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Carga el componente desde un archivo.
|
||||||
|
*
|
||||||
|
* @param string $component
|
||||||
|
*
|
||||||
|
* @return static
|
||||||
|
*/
|
||||||
|
public function load(string $component): static
|
||||||
|
{
|
||||||
|
$componentRealName = trim(str_replace(':', '/', $component), ' :');
|
||||||
|
if (file_exists($this->viewPath.$componentRealName.$this->extension)) {
|
||||||
|
ob_start();
|
||||||
|
include($this->viewPath.$componentRealName.$this->extension);
|
||||||
|
$this->content = ob_get_clean();
|
||||||
|
$this->parse();
|
||||||
|
} else {
|
||||||
|
throw new \Exception(
|
||||||
|
'"'.$component.'" component not exists.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parsea y procesa el contenido en busca de más componentes
|
||||||
|
*
|
||||||
|
* @param array $components
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
protected function parse(array $components = null): void
|
||||||
|
{
|
||||||
|
if (empty($components)) {
|
||||||
|
preg_match_all(
|
||||||
|
'/<(([\:]+)?[A-Z][\w1-9:]+) ?([^>]+)?>/s',
|
||||||
|
$this->content, $matches, PREG_PATTERN_ORDER
|
||||||
|
);
|
||||||
|
$components = $matches[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (array_unique($components) as $component) {
|
||||||
|
$this->parseComponent($component);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parsea y procesa un componente
|
||||||
|
*
|
||||||
|
* @param string $component
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
protected function parseComponent(
|
||||||
|
string $component,
|
||||||
|
): void
|
||||||
|
{
|
||||||
|
$this->content = preg_replace_callback(
|
||||||
|
'/<'.$component.'([^>]+)?>(.+)?<\/'.$component.'>/sU',
|
||||||
|
function($matches) use($component) {
|
||||||
|
$properties = isset($matches[1]) ? static::parseProperties($matches[1]) : [];
|
||||||
|
$instance = new static($this->properties);
|
||||||
|
$instance->content = $matches[2] ?? '';
|
||||||
|
$instance->addProperties($properties);
|
||||||
|
$instance->load($component);
|
||||||
|
return $instance->getContent();
|
||||||
|
},
|
||||||
|
$this->content
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convierte un a cadena en formato "clave='valor' clave2='valor2'" en un array.
|
||||||
|
*
|
||||||
|
* @param string $propertiesString
|
||||||
|
*
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
protected static function parseProperties(string $propertiesString): array
|
||||||
|
{
|
||||||
|
preg_match_all('/([\w]+)=[\'"](.+)?[\'"]/sU', $propertiesString, $matches, PREG_PATTERN_ORDER);
|
||||||
|
$result = [];
|
||||||
|
foreach($matches[1] as $index => $property) {
|
||||||
|
$result[$property] = $matches[2][$index];
|
||||||
|
}
|
||||||
|
preg_match_all('/([\w]+) /si', $propertiesString.' ', $matches, PREG_PATTERN_ORDER);
|
||||||
|
foreach($matches[1] as $property) {
|
||||||
|
$result[$property] = $property;
|
||||||
|
}
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Imprime el componente
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function print(): void
|
||||||
|
{
|
||||||
|
print($this->content);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renderiza un componente
|
||||||
|
*
|
||||||
|
* @param string $component
|
||||||
|
* @param array $properties
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public static function render(string $component, array $properties = []): void
|
||||||
|
{
|
||||||
|
$instance = new static($properties);
|
||||||
|
$instance->load($component);
|
||||||
|
$instance->print();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Devuelve el componente como string
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function getContent(): string
|
||||||
|
{
|
||||||
|
return $this->content;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Devuelve el valor de una propiedad.
|
||||||
|
*
|
||||||
|
* @param string $key
|
||||||
|
* @param mixed $default
|
||||||
|
*
|
||||||
|
* @return mixed
|
||||||
|
*/
|
||||||
|
public function property(string $key, mixed $default = null): mixed
|
||||||
|
{
|
||||||
|
return $this->properties[$key] ?? $default;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Devuelve el valor de una propiedad escapado con htmlspecialchars.
|
||||||
|
*
|
||||||
|
* @param string $key
|
||||||
|
* @param string $default
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function escapedProperty(string $key, string $default = ''): string
|
||||||
|
{
|
||||||
|
if (isset($this->properties[$key]))
|
||||||
|
return htmlspecialchars($this->properties[$key] ?? '');
|
||||||
|
else
|
||||||
|
return htmlspecialchars($default);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Función alias/apócope de la función property.
|
||||||
|
*
|
||||||
|
* @param string $key
|
||||||
|
* @param mixed $default
|
||||||
|
*
|
||||||
|
* @return mixed
|
||||||
|
*/
|
||||||
|
public function p(string $key, mixed $default = null): mixed
|
||||||
|
{
|
||||||
|
return $this->property($key, $default);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Función alias/apócope de la función escapedProperty.
|
||||||
|
*
|
||||||
|
* @param string $key
|
||||||
|
* @param string $default
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function e(string $key, string $default = ''): string
|
||||||
|
{
|
||||||
|
return $this->escapedProperty($key, $default);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Según el array de nombres de propiedades recibido, los devuelve
|
||||||
|
* con sus valores como array asociativo.
|
||||||
|
* Si no recibe ningua propiedad como argumento, devuelve todas las existentes.
|
||||||
|
*
|
||||||
|
* @param array $properties
|
||||||
|
*
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function propsToArray(...$properties): array
|
||||||
|
{
|
||||||
|
if (empty($properties))
|
||||||
|
return $this->properties;
|
||||||
|
|
||||||
|
$result = [];
|
||||||
|
foreach ($properties as $property)
|
||||||
|
$result[$property] = $this->properties[$property] ?? null;
|
||||||
|
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Según el array de nombres de propiedades recibido, devuelve
|
||||||
|
* como atributos HTML en formato propiedad="valor".
|
||||||
|
*
|
||||||
|
* @param array $properties
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function propsToAtts(...$properties): string
|
||||||
|
{
|
||||||
|
$result = ' ';
|
||||||
|
foreach ($properties as $property)
|
||||||
|
$result .= $property.'="'.htmlspecialchars($this->property($property, '')).'" ';
|
||||||
|
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Según el array de nombres de propiedades recibido, devuelve
|
||||||
|
* como atributos HTML unitarios como checked, required, etc.
|
||||||
|
*
|
||||||
|
* @param array $properties
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function propsToUnary(...$properties): string
|
||||||
|
{
|
||||||
|
$result = ' ';
|
||||||
|
foreach ($properties as $property)
|
||||||
|
if (isset($this->properties[$property]))
|
||||||
|
$result .= $property.' ';
|
||||||
|
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Trata una propiedad como una definición de propiedad HTML unitaria.
|
||||||
|
*
|
||||||
|
* @param string $property Propiedad unitaria buscar.
|
||||||
|
* @param mixed $result Valor a devolver en caso de estar definda la propiedad. Si es nulo, devuelve $property.
|
||||||
|
*
|
||||||
|
* @return mixed
|
||||||
|
*/
|
||||||
|
public function unary(string $property, mixed $result = null): mixed
|
||||||
|
{
|
||||||
|
if (isset($this->properties[$property])) {
|
||||||
|
if (isset($result))
|
||||||
|
return $result;
|
||||||
|
else
|
||||||
|
return $property;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Añade una nueva propiedad/atributo.
|
||||||
|
* En caso de que una propiedad exista, la reemplaza.
|
||||||
|
*
|
||||||
|
* @param string $key
|
||||||
|
* @param mixed $value
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function addProperty(string $key, mixed $value): void
|
||||||
|
{
|
||||||
|
$this->properties = array_merge($this->properties, [$key => $value]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Añade nuevas propiedades en lote.
|
||||||
|
* En caso de que una propiedad exista, la reemplaza.
|
||||||
|
*
|
||||||
|
* @param array $properties
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function addProperties(array $properties): void
|
||||||
|
{
|
||||||
|
$this->properties = array_merge($this->properties, $properties);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Intenta devolver la url absoluta a partir de una ruta relativa.
|
||||||
|
*
|
||||||
|
* @param string $path
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public static function route(string $path = '/'): string
|
||||||
|
{
|
||||||
|
if (defined('SITE_URL') && !empty(SITE_URL))
|
||||||
|
return rtrim(SITE_URL, '/').'/'.ltrim($path, '/');
|
||||||
|
|
||||||
|
return $path;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Devuelve la ruta de la petición.
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public static function path(): string
|
||||||
|
{
|
||||||
|
return Router::currentPath();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* __get
|
||||||
|
*
|
||||||
|
* @param string $index
|
||||||
|
* @return mixed
|
||||||
|
*/
|
||||||
|
public function __get(string $index): mixed
|
||||||
|
{
|
||||||
|
$value = $this->property($index);
|
||||||
|
|
||||||
|
if (is_string($value))
|
||||||
|
return htmlspecialchars($value);
|
||||||
|
else
|
||||||
|
return $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* __isset
|
||||||
|
*
|
||||||
|
* @param string $key
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function __isset(string $key): bool
|
||||||
|
{
|
||||||
|
return isset($this->properties[$key]);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,291 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* HTMX - DuckBrain
|
||||||
|
*
|
||||||
|
* Librería de componentes HTMX para duckbrain.
|
||||||
|
*
|
||||||
|
* @author KJ
|
||||||
|
* @website https://kj2.me
|
||||||
|
* @licence MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Libs;
|
||||||
|
|
||||||
|
class HTMX extends HTMLComponent {
|
||||||
|
/**
|
||||||
|
* Renderiza un componente
|
||||||
|
*
|
||||||
|
* @param string $component
|
||||||
|
* @param array $properties
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public static function render(
|
||||||
|
string $component,
|
||||||
|
array $properties = []
|
||||||
|
): void
|
||||||
|
{
|
||||||
|
$instance = new static($properties);
|
||||||
|
$instance->load($component);
|
||||||
|
$instance->print();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parsea y procesa el contenido en busca de más componentes
|
||||||
|
*
|
||||||
|
* @param array $components
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
protected function parse(array $components = null): void
|
||||||
|
{
|
||||||
|
$this->parseHTMX();
|
||||||
|
parent::parse();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Elimina o muestra secciones según etiquetas de comentarios y
|
||||||
|
* los estados de la petición HTMX.
|
||||||
|
*
|
||||||
|
* Los comentarios posibles son:
|
||||||
|
*
|
||||||
|
* | Comentario | Descripción |
|
||||||
|
* |------------------+---------------------------------------------------|
|
||||||
|
* | htmx | Solo se muestra si la petición es HTMX |
|
||||||
|
* | nothtmx | Solo se muestra si la petición no es HTMX |
|
||||||
|
* | boosted | Solo se muestra si la petición es boosted |
|
||||||
|
* | notboosted | Solo se muestra si la petición no es boosted |
|
||||||
|
* | nothtmxorboosted | Se muestra si la petición no es HTMX o es boosted |
|
||||||
|
* | htmxornotboosted | Se muestra si la petición es HTMX o no es boosted |
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
protected function parseHTMX(): void {
|
||||||
|
if (static::isHtmx())
|
||||||
|
if (static::isBoosted())
|
||||||
|
$replacement = ['$3', '', '$3', '', '$3', ''];
|
||||||
|
else
|
||||||
|
$replacement = ['$3', '', '', '$3', '', '$3'];
|
||||||
|
else
|
||||||
|
$replacement = ['', '$3', '', '$3', '$3', ''];
|
||||||
|
|
||||||
|
$this->content = trim(preg_replace(
|
||||||
|
[
|
||||||
|
'/([\t \r\n]+)?<!-- htmx -->([\t \r\n]+)?(.+)?([\t \r\n]+)?<!-- \/htmx -->([\t \r\n]+)?/siU',
|
||||||
|
'/([\t \r\n]+)?<!-- nothtmx -->([\t \r\n]+)?(.+)?([\t \r\n]+)?<!-- \/nothtmx -->([\t \r\n]+)?/siU',
|
||||||
|
'/([\t \r\n]+)?<!-- boosted -->([\t \r\n]+)?(.+)?([\t \r\n]+)?<!-- \/boosted -->([\t \r\n]+)?/siU',
|
||||||
|
'/([\t \r\n]+)?<!-- notboosted -->([\t \r\n]+)?(.+)?([\t \r\n]+)?<!-- \/notboosted -->([\t \r\n]+)?/siU',
|
||||||
|
'/([\t \r\n]+)?<!-- nothtmxorboosted -->([\t \r\n]+)?(.+)?([\t \r\n]+)?<!-- \/nothtmxorboosted -->([\t \r\n]+)?/siU',
|
||||||
|
'/([\t \r\n]+)?<!-- htmxornotboosted -->([\t \r\n]+)?(.+)?([\t \r\n]+)?<!-- \/htmxornotboosted -->([\t \r\n]+)?/siU',
|
||||||
|
],
|
||||||
|
$replacement,
|
||||||
|
$this->content
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Elimina las líneas en blanco.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function deleteEmptyLines(): void
|
||||||
|
{
|
||||||
|
$this->content = trim(preg_replace(
|
||||||
|
'/^[ \t]*[\r\n]+/m',
|
||||||
|
'',
|
||||||
|
$this->content
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verifica si la petición es o no htmx.
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public static function isHtmx(): bool
|
||||||
|
{
|
||||||
|
return isset($_SERVER['HTTP_HX_REQUEST']);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verifica que la petición sea HTMX pero no boosteada.
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public static function isHtmxNotBoosted(): bool
|
||||||
|
{
|
||||||
|
return static::isHtmx() && !static::isBoosted();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verifica que la petición no sea HTMX o sea Boosteada.
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public static function notHtmxOrBoosted(): bool
|
||||||
|
{
|
||||||
|
return !static::isHtmx() || static::isBoosted();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Redirije a una ruta relativa interna enviando el header adecuado
|
||||||
|
* si es una petición normal o htmx.
|
||||||
|
*
|
||||||
|
* @param string $path
|
||||||
|
* La ruta relativa a la ruta base.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public static function redirect(string $path): void
|
||||||
|
{
|
||||||
|
if (static::isHtmx())
|
||||||
|
header('HX-Redirect: '.Router::basePath().ltrim($path, '/'));
|
||||||
|
else
|
||||||
|
Router::redirect($path);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verifica si la petición es o no Boosted.
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public static function isBoosted(): bool
|
||||||
|
{
|
||||||
|
return isset($_SERVER['HTTP_HX_BOOSTED']) && boolval($_SERVER['HTTP_HX_BOOSTED']);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Devuelve la url actual del navegador cuando se hace la petición HTMX.
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public static function currentURL(): string
|
||||||
|
{
|
||||||
|
return $_SERVER['HTTP_HX_CURRENT_URL'] ?? '';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Respuesta del hx-prompt si es que ha sido usado.
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public static function prompt(): string
|
||||||
|
{
|
||||||
|
return $_SERVER['HTTP_HX_PROMPT'] ?? '';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Devuelve el id del target HTMX (si existe).
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public static function target(): string
|
||||||
|
{
|
||||||
|
return $_SERVER['HTTP_HX_TARGET'] ?? '';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* El nombre del elemento que levanta la petición (si existe).
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public static function triggerName(): string
|
||||||
|
{
|
||||||
|
return $_SERVER['HTTP_HX_TRIGGER_NAME'] ?? '';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* El id del elemento que levanta la petición (si existe).
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public static function triggerId(): string
|
||||||
|
{
|
||||||
|
return $_SERVER['HTTP_HX_TRIGGER'] ?? '';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Solo cuando es una petición HTMX devuelve la propiedad HTMX hx-swap-oob.
|
||||||
|
*
|
||||||
|
* @param string $value Valor de hx-swap (por defecto: true).
|
||||||
|
* @param bool $excludeBoosted Excluir si la petición es boosted (por defecto: false)
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public static function swapOob(string $value = 'true', bool $excludeBoosted = false): string
|
||||||
|
{
|
||||||
|
if ($excludeBoosted && static::isBoosted())
|
||||||
|
return '';
|
||||||
|
|
||||||
|
return static::isHtmx() ? 'hx-swap-oob="'.$value.'"' : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Solo cuando es una petición HTMX devuelve la propiedad HTMX hx-select-oob.
|
||||||
|
*
|
||||||
|
* @param string $selector
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public static function selectOob(string $selector): string
|
||||||
|
{
|
||||||
|
return static::isHtmx() ? 'hx-select-oob="'.$selector.'"' : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fuerza a cambiar la URL del navegador y la coloca en el historial
|
||||||
|
* mientras que HTMX crea un caché de la página para cuando se de clic
|
||||||
|
* en regresar.
|
||||||
|
*
|
||||||
|
* Equivamente a usar la propiedad htmx hx-push-url.
|
||||||
|
*
|
||||||
|
* @param string $url Ruta relativa.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public static function pushUrl(string $url): void
|
||||||
|
{
|
||||||
|
if (static::isHtmx())
|
||||||
|
header('HX-Push-Url: '.static::route($url));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fuerza a cambiar la URL del navegador y pero sin colocarla en el historial.
|
||||||
|
*
|
||||||
|
* Equivamente a usar la propiedad htmx hx-push-url.
|
||||||
|
*
|
||||||
|
* @param string $url Ruta relativa.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public static function replaceUrl(string $url): void
|
||||||
|
{
|
||||||
|
if (static::isHtmx())
|
||||||
|
header('HX-Replace-Url: '.static::route($url));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cambia el swap target de HTMX.
|
||||||
|
*
|
||||||
|
* @param string $selector
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public static function retarget(string $selector): void
|
||||||
|
{
|
||||||
|
header('HX-Retarget: '.$selector);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cambia el swap rule de HTMX.
|
||||||
|
*
|
||||||
|
* @param string $rule
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public static function reswap(string $rule = 'innerHTML'): void
|
||||||
|
{
|
||||||
|
header('HX-Reswap: '.$rule);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,28 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Middleware - DuckBrain
|
||||||
|
*
|
||||||
|
* Librería base para middlewares.
|
||||||
|
*
|
||||||
|
* @author KJ
|
||||||
|
* @website https://kj2.me
|
||||||
|
* @licence MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Libs;
|
||||||
|
|
||||||
|
class Middleware {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Llama al siguiente callback.
|
||||||
|
*
|
||||||
|
* @param Neuron $req
|
||||||
|
*
|
||||||
|
* @return mixed
|
||||||
|
*/
|
||||||
|
public static function next(Neuron $req): mixed
|
||||||
|
{
|
||||||
|
$next = array_pop($req->next);
|
||||||
|
return call_user_func_array($next, [$req]);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,938 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Model - DuckBrain
|
||||||
|
*
|
||||||
|
* Modelo ORM para objetos que hagan uso de una base de datos.
|
||||||
|
* Depende de Libs\Database y hace uso de las constantes
|
||||||
|
* DB_TYPE, DB_HOST, DB_NAME, DB_USER y DB_PASS.
|
||||||
|
*
|
||||||
|
* @author KJ
|
||||||
|
* @website https://kj2.me
|
||||||
|
* @licence MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Libs;
|
||||||
|
|
||||||
|
use Libs\Database;
|
||||||
|
use PDO;
|
||||||
|
use PDOException;
|
||||||
|
use Exception;
|
||||||
|
use ReflectionClass;
|
||||||
|
use ReflectionProperty;
|
||||||
|
use AllowDynamicProperties;
|
||||||
|
|
||||||
|
#[AllowDynamicProperties]
|
||||||
|
class Model {
|
||||||
|
|
||||||
|
public ?int $id = null;
|
||||||
|
protected array $toNull = [];
|
||||||
|
static protected string $primaryKey = 'id';
|
||||||
|
static protected array $ignoreSave = ['id'];
|
||||||
|
static protected array $forceSave = [];
|
||||||
|
static protected string $table;
|
||||||
|
static protected string $tableSufix = 's';
|
||||||
|
static protected array $queryVars = [];
|
||||||
|
static protected array $querySelect = [
|
||||||
|
'select' => ['*'],
|
||||||
|
'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 "<pre>";
|
||||||
|
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) && isset($value) &&
|
||||||
|
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(
|
||||||
|
'/(?<!^)[A-Z]/', '_$0',
|
||||||
|
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.
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
protected function update(): void
|
||||||
|
{
|
||||||
|
$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.
|
||||||
|
* @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<static>
|
||||||
|
* 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<static>
|
||||||
|
* 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
?>
|
|
@ -0,0 +1,54 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Neuron - DuckBrain
|
||||||
|
*
|
||||||
|
* Neuron, sirve para crear un objeto que alojará valores, pero
|
||||||
|
* además tiene la característica especial de que al intentar
|
||||||
|
* acceder a un atributo que no está definido devolerá nulo en
|
||||||
|
* lugar de generar un error php notice que indica que se está
|
||||||
|
* intentando acceder a un valor no definido.
|
||||||
|
*
|
||||||
|
* El constructor recibe un objeto o arreglo con los valores que
|
||||||
|
* sí estarán definidos.
|
||||||
|
*
|
||||||
|
* @author KJ
|
||||||
|
* @website https://kj2.me
|
||||||
|
* @licence MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Libs;
|
||||||
|
use AllowDynamicProperties;
|
||||||
|
|
||||||
|
#[AllowDynamicProperties]
|
||||||
|
class Neuron {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* __construct
|
||||||
|
*
|
||||||
|
* @param array $data
|
||||||
|
*/
|
||||||
|
public function __construct(...$data)
|
||||||
|
{
|
||||||
|
if (count($data) === 1 &&
|
||||||
|
isset($data[0]) &&
|
||||||
|
(is_array($data[0]) ||
|
||||||
|
is_object($data[0])))
|
||||||
|
$data = $data[0];
|
||||||
|
|
||||||
|
foreach($data as $key => $value)
|
||||||
|
$this->{$key} = $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* __get
|
||||||
|
*
|
||||||
|
* @param string $index
|
||||||
|
* @return null
|
||||||
|
*/
|
||||||
|
public function __get(string $index): null
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
?>
|
|
@ -0,0 +1,144 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Request - DuckBrain
|
||||||
|
*
|
||||||
|
* Libería complementaria de la libería Router.
|
||||||
|
* Contiene el cuerpo básico de la petición http (POST, GET, JSON, etc).
|
||||||
|
*
|
||||||
|
* @author KJ
|
||||||
|
* @website https://kj2.me
|
||||||
|
* @licence MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Libs;
|
||||||
|
|
||||||
|
class Request extends Neuron {
|
||||||
|
public Neuron $get;
|
||||||
|
public Neuron $post;
|
||||||
|
public Neuron $put;
|
||||||
|
public Neuron $patch;
|
||||||
|
public Neuron $json;
|
||||||
|
public Neuron $params;
|
||||||
|
public string $path;
|
||||||
|
public string $error;
|
||||||
|
public array $next;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* __construct
|
||||||
|
*
|
||||||
|
* @param string $path Ruta actual tomando como raíz la instalación de DuckBrain.
|
||||||
|
*/
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
$this->path = Router::currentPath();
|
||||||
|
$this->get = new Neuron($_GET);
|
||||||
|
$this->post = new Neuron($_POST);
|
||||||
|
$this->put = new Neuron();
|
||||||
|
$this->patch = new Neuron();
|
||||||
|
|
||||||
|
$contentType = isset($_SERVER["CONTENT_TYPE"]) ? trim($_SERVER["CONTENT_TYPE"]) : '';
|
||||||
|
if ($contentType === "application/json")
|
||||||
|
$this->json = new Neuron(
|
||||||
|
(object) json_decode(trim(file_get_contents("php://input")), false)
|
||||||
|
);
|
||||||
|
else {
|
||||||
|
$this->json = new Neuron();
|
||||||
|
if (in_array($_SERVER['REQUEST_METHOD'], ['PUT', 'PATCH'])) {
|
||||||
|
parse_str(file_get_contents("php://input"), $input_vars);
|
||||||
|
$this->{strtolower($_SERVER['REQUEST_METHOD'])} = new Neuron($input_vars);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->params = new Neuron();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Corre las validaciones e intenta continuar con la pila de callbacks.
|
||||||
|
*
|
||||||
|
* @return mixed
|
||||||
|
*/
|
||||||
|
public function handle(): mixed
|
||||||
|
{
|
||||||
|
if ($this->validate())
|
||||||
|
return Middleware::next($this);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Inicia la validación que se haya configurado.
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function validate(): bool
|
||||||
|
{
|
||||||
|
$actual = match($_SERVER['REQUEST_METHOD']) {
|
||||||
|
'GET', 'DELETE' => $this->get,
|
||||||
|
default => $this->{strtolower($_SERVER['REQUEST_METHOD'])}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (Validator::validateList(static::paramRules(), $this->params) &&
|
||||||
|
Validator::validateList(static::getRules(), $this->get ) &&
|
||||||
|
Validator::validateList(static::rules(), $actual))
|
||||||
|
return true;
|
||||||
|
|
||||||
|
if (isset(static::messages()[Validator::$lastFailed]))
|
||||||
|
$error = static::messages()[Validator::$lastFailed];
|
||||||
|
else {
|
||||||
|
|
||||||
|
$error = 'Error: validation failed of '.preg_replace('/\./', ' as ', Validator::$lastFailed, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
return static::onInvalid($error);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reglas para el método actual.
|
||||||
|
*
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public static function rules(): array {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reglas para los parámetros por URL.
|
||||||
|
*
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public static function paramRules(): array {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reglas para los parámetros GET.
|
||||||
|
*
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public static function getRules(): array {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mensajes de error en caso de fallar una validación.
|
||||||
|
*
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public static function messages(): array {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Función a ejecutar cuando se ha detectado un valor no válido.
|
||||||
|
*
|
||||||
|
* @param string $error
|
||||||
|
*
|
||||||
|
* @return false
|
||||||
|
*/
|
||||||
|
protected function onInvalid(string $error): false
|
||||||
|
{
|
||||||
|
http_response_code(422);
|
||||||
|
print($error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,374 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Router - DuckBrain
|
||||||
|
*
|
||||||
|
* Librería de Enrrutador.
|
||||||
|
* Depende de manera forzada de que la constante ROOT_DIR esté definida
|
||||||
|
* y de manera optativa de que la constante SITE_URL lo esté también.
|
||||||
|
*
|
||||||
|
* @author KJ
|
||||||
|
* @website https://kj2.me
|
||||||
|
* @licence MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Libs;
|
||||||
|
|
||||||
|
class Router {
|
||||||
|
private static $get = [];
|
||||||
|
private static $post = [];
|
||||||
|
private static $put = [];
|
||||||
|
private static $patch = [];
|
||||||
|
private static $delete = [];
|
||||||
|
private static $last;
|
||||||
|
public static $notFoundCallback = 'Libs\Router::defaultNotFound';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Función callback por defectio para cuando
|
||||||
|
* no se encuentra configurada la ruta.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public static function defaultNotFound (): void
|
||||||
|
{
|
||||||
|
header("HTTP/1.0 404 Not Found");
|
||||||
|
echo '<h2 style="text-align: center;margin: 25px 0px;">Error 404 - Página no encontrada</h2>';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* __construct
|
||||||
|
*/
|
||||||
|
private function __construct() {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parsea para deectar las pseudovariables (ej: {variable})
|
||||||
|
*
|
||||||
|
* @param string $path
|
||||||
|
* Ruta con pseudovariables.
|
||||||
|
*
|
||||||
|
* @param callable $callback
|
||||||
|
* Callback que será llamado cuando la ruta configurada en $path coincida.
|
||||||
|
*
|
||||||
|
* @return array
|
||||||
|
* Arreglo con 2 índices:
|
||||||
|
* path - Contiene la ruta con las pseudovariables reeplazadas por expresiones regulares.
|
||||||
|
* callback - Contiene el callback en formato Namespace\Clase::Método.
|
||||||
|
*/
|
||||||
|
private static function parse(string $path, callable $callback): array
|
||||||
|
{
|
||||||
|
preg_match_all('/{(\w+)}/s', $path, $matches, PREG_PATTERN_ORDER);
|
||||||
|
$paramNames = $matches[1];
|
||||||
|
|
||||||
|
$path = preg_quote($path, '/');
|
||||||
|
$path = preg_replace(
|
||||||
|
['/\\\{\w+\\\}/s'],
|
||||||
|
['([^\/]+)'],
|
||||||
|
$path);
|
||||||
|
|
||||||
|
return [
|
||||||
|
'path' => $path,
|
||||||
|
'callback' => [$callback],
|
||||||
|
'paramNames' => $paramNames
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Devuelve el ruta base o raiz del proyecto sobre la que trabajará el router.
|
||||||
|
*
|
||||||
|
* Ej: Si la url del sistema está en "https://ejemplo.com/duckbrain"
|
||||||
|
* entonces la ruta base sería "/duckbrain"
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public static function basePath(): string
|
||||||
|
{
|
||||||
|
if (defined('SITE_URL') && !empty(SITE_URL))
|
||||||
|
return rtrim(parse_url(SITE_URL, PHP_URL_PATH), '/').'/';
|
||||||
|
return str_replace($_SERVER['DOCUMENT_ROOT'], '/', ROOT_DIR);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Redirije a una ruta relativa interna.
|
||||||
|
*
|
||||||
|
* @param string $path
|
||||||
|
* La ruta relativa a la ruta base.
|
||||||
|
*
|
||||||
|
* Ej: Si nuesto sistema está en "https://ejemplo.com/duckbrain"
|
||||||
|
* llamamos a Router::redirect('/docs'), entonces seremos
|
||||||
|
* redirigidos a "https://ejemplo.com/duckbrain/docs".
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public static function redirect(string $path): void
|
||||||
|
{
|
||||||
|
header('Location: '.static::basePath().ltrim($path, '/'));
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Añade un middleware a la última ruta usada.
|
||||||
|
* Solo se puede usar un middleware a la vez.
|
||||||
|
*
|
||||||
|
* @param callable $callback
|
||||||
|
* @param int $prioriry
|
||||||
|
*
|
||||||
|
* @return static
|
||||||
|
* Devuelve la instancia actual.
|
||||||
|
*/
|
||||||
|
public static function middleware(callable $callback, int $priority = null): static
|
||||||
|
{
|
||||||
|
if (!isset(static::$last))
|
||||||
|
return new static();
|
||||||
|
|
||||||
|
$method = static::$last[0];
|
||||||
|
$index = static::$last[1];
|
||||||
|
|
||||||
|
if (isset($priority) && $priority <= 0)
|
||||||
|
$priority = 1;
|
||||||
|
|
||||||
|
if (is_null($priority) || $priority >= count(static::$$method[$index]['callback']))
|
||||||
|
static::$$method[$index]['callback'][] = $callback;
|
||||||
|
else {
|
||||||
|
static::$$method[$index]['callback'] = array_merge(
|
||||||
|
array_slice(static::$$method[$index]['callback'], 0, $priority),
|
||||||
|
[$callback],
|
||||||
|
array_slice(static::$$method[$index]['callback'], $priority)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new static();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reconfigura el callback final de la última ruta.
|
||||||
|
*
|
||||||
|
* @param callable $callback
|
||||||
|
*
|
||||||
|
* @return static
|
||||||
|
*/
|
||||||
|
public static function reconfigure(callable $callback): static
|
||||||
|
{
|
||||||
|
if (empty(static::$last))
|
||||||
|
return new static();
|
||||||
|
|
||||||
|
$method = static::$last[0];
|
||||||
|
$index = static::$last[1];
|
||||||
|
|
||||||
|
static::$$method[$index]['callback'][0] = $callback;
|
||||||
|
|
||||||
|
return new static();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configura calquier método para todas las rutas.
|
||||||
|
*
|
||||||
|
* En caso de no recibir un callback, busca la ruta actual
|
||||||
|
* solo configura la ruta como la última configurada
|
||||||
|
* siempre y cuando la misma haya sido configurada previamente.
|
||||||
|
*
|
||||||
|
* @param string $method
|
||||||
|
* Método http.
|
||||||
|
* @param string $path
|
||||||
|
* Ruta con pseudovariables.
|
||||||
|
* @param callable|null $callback
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
* Devuelve la instancia actual.
|
||||||
|
*/
|
||||||
|
public static function configure(string $method, string $path, ?callable $callback = null): static
|
||||||
|
{
|
||||||
|
if (is_null($callback)) {
|
||||||
|
$path = preg_quote($path, '/');
|
||||||
|
$path = preg_replace(
|
||||||
|
['/\\\{\w+\\\}/s'],
|
||||||
|
['([^\/]+)'],
|
||||||
|
$path);
|
||||||
|
|
||||||
|
foreach(static::$$method as $index => $router)
|
||||||
|
if ($router['path'] == $path) {
|
||||||
|
static::$last = [$method, $index];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new static();
|
||||||
|
}
|
||||||
|
|
||||||
|
static::$$method[] = static::parse($path, $callback);
|
||||||
|
static::$last = [$method, count(static::$$method)-1];
|
||||||
|
return new static();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Define los routers para el método GET.
|
||||||
|
*
|
||||||
|
* @param string $path
|
||||||
|
* Ruta con pseudovariables.
|
||||||
|
* @param callable $callback
|
||||||
|
* Callback que será llamado cuando la ruta configurada en $path coincida.
|
||||||
|
*
|
||||||
|
* @return static
|
||||||
|
* Devuelve la instancia actual.
|
||||||
|
*/
|
||||||
|
public static function get(string $path, callable $callback = null): static
|
||||||
|
{
|
||||||
|
return static::configure('get', $path, $callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Define los routers para el método POST.
|
||||||
|
*
|
||||||
|
* @param string $path
|
||||||
|
* Ruta con pseudovariables.
|
||||||
|
* @param callable $callback
|
||||||
|
* Callback que será llamado cuando la ruta configurada en $path coincida.
|
||||||
|
*
|
||||||
|
* @return static
|
||||||
|
* Devuelve la instancia actual.
|
||||||
|
*/
|
||||||
|
public static function post(string $path, callable $callback = null): static
|
||||||
|
{
|
||||||
|
return static::configure('post', $path, $callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Define los routers para el método PUT.
|
||||||
|
*
|
||||||
|
* @param string $path
|
||||||
|
* Ruta con pseudovariables.
|
||||||
|
* @param callable $callback
|
||||||
|
* Callback que será llamado cuando la ruta configurada en $path coincida.
|
||||||
|
*
|
||||||
|
* @return static
|
||||||
|
* Devuelve la instancia actual
|
||||||
|
*/
|
||||||
|
|
||||||
|
public static function put(string $path, callable $callback = null): static
|
||||||
|
{
|
||||||
|
return static::configure('put', $path, $callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Define los routers para el método PATCH.
|
||||||
|
*
|
||||||
|
* @param string $path
|
||||||
|
* Ruta con pseudovariables.
|
||||||
|
* @param callable $callback
|
||||||
|
* Callback que será llamado cuando la ruta configurada en $path coincida.
|
||||||
|
*
|
||||||
|
* @return static
|
||||||
|
* Devuelve la instancia actual
|
||||||
|
*/
|
||||||
|
public static function patch(string $path, callable $callback = null): static
|
||||||
|
{
|
||||||
|
return static::configure('patch', $path, $callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Define los routers para el método DELETE.
|
||||||
|
*
|
||||||
|
* @param string $path
|
||||||
|
* Ruta con pseudovariables
|
||||||
|
* @param callable $callback
|
||||||
|
* Callback que será llamado cuando la ruta configurada en $path coincida.
|
||||||
|
*
|
||||||
|
* @return static
|
||||||
|
* Devuelve la instancia actual
|
||||||
|
*/
|
||||||
|
public static function delete(string $path, callable $callback = null): static
|
||||||
|
{
|
||||||
|
return static::configure('delete', $path, $callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Devuelve la ruta actual tomando como raíz la ruta de instalación de DuckBrain.
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public static function currentPath() : string
|
||||||
|
{
|
||||||
|
return preg_replace('/'.preg_quote(static::basePath(), '/').'/',
|
||||||
|
'/', strtok($_SERVER['REQUEST_URI'], '?'), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Aplica la configuración de rutas.
|
||||||
|
*
|
||||||
|
* @param string $path (opcional) Ruta a usar. Si no se define, detecta la ruta actual.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public static function apply(string $path = null): void
|
||||||
|
{
|
||||||
|
$path = $path ?? static::currentPath();
|
||||||
|
$routers = match($_SERVER['REQUEST_METHOD']) { // Según el método selecciona un arreglo de routers
|
||||||
|
'POST' => static::$post,
|
||||||
|
'PUT' => static::$put,
|
||||||
|
'PATCH' => static::$patch,
|
||||||
|
'DELETE' => static::$delete,
|
||||||
|
default => static::$get
|
||||||
|
};
|
||||||
|
|
||||||
|
foreach ($routers as $router) { // revisa todos los routers para ver si coinciden con la ruta actual
|
||||||
|
if (preg_match_all('/^'.$router['path'].'\/?$/si',$path, $matches, PREG_PATTERN_ORDER)) {
|
||||||
|
unset($matches[0]);
|
||||||
|
|
||||||
|
// Objtener un reflection del callback
|
||||||
|
$lastCallback = $router['callback'][0];
|
||||||
|
if ($lastCallback instanceof \Closure) { // si es función anónima
|
||||||
|
$reflectionCallback = new \ReflectionFunction($lastCallback);
|
||||||
|
} else {
|
||||||
|
if (is_string($lastCallback))
|
||||||
|
$lastCallback = preg_split('/::/', $lastCallback);
|
||||||
|
|
||||||
|
// Revisamos su es un método o solo una función
|
||||||
|
if (count($lastCallback) == 2)
|
||||||
|
$reflectionCallback = new \ReflectionMethod($lastCallback[0], $lastCallback[1]);
|
||||||
|
else
|
||||||
|
$reflectionCallback = new \ReflectionFunction($lastCallback[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Obtener los parámetros
|
||||||
|
$arguments = $reflectionCallback->getParameters();
|
||||||
|
if (isset($arguments[0])) {
|
||||||
|
// Obtenemos la clase del primer parámetro
|
||||||
|
$argumentClass = strval($arguments[0]->getType());
|
||||||
|
|
||||||
|
// Verificamos si la clase está o no tipada
|
||||||
|
if (empty($argumentClass)) {
|
||||||
|
$request = new Request;
|
||||||
|
} else {
|
||||||
|
$request = new $argumentClass;
|
||||||
|
|
||||||
|
// Verificamos que sea instancia de Request (requerimiento)
|
||||||
|
if (!($request instanceof Request))
|
||||||
|
throw new \Exception('Bad argument type on router callback.');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$request = new Request;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Comprobando y guardando los parámetros variables de la ruta
|
||||||
|
if (isset($matches[1])) {
|
||||||
|
foreach ($matches as $index => $match) {
|
||||||
|
$paramName = $router['paramNames'][$index-1];
|
||||||
|
$request->params->$paramName = urldecode($match[0]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Llama a la validación y luego procesa la cola de callbacks
|
||||||
|
$request->next = $router['callback'];
|
||||||
|
$data = $request->handle();
|
||||||
|
|
||||||
|
// Por defecto imprime como JSON si se retorna algo
|
||||||
|
if (isset($data)) {
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
print(json_encode($data));
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Si no hay router que coincida llamamos a $notFoundCallBack
|
||||||
|
call_user_func_array(static::$notFoundCallback, [new Request]);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,201 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Validator - DuckBrain
|
||||||
|
*
|
||||||
|
* Libería complementaria de la libería Request.
|
||||||
|
* Sirve para simplpificar la verificación de valores.
|
||||||
|
*
|
||||||
|
* Tiene la posibilida de verificar tanto reglas individuales como en lote.
|
||||||
|
*
|
||||||
|
* |----------+--------------------------------------------------------|
|
||||||
|
* | Regla | Descripción |
|
||||||
|
* |----------+--------------------------------------------------------|
|
||||||
|
* | not | Niega la siguiente regla. Ej: not:float |
|
||||||
|
* | exists | Es requerido; debe estar definido y puede estar vacío |
|
||||||
|
* | required | Es requerido; debe estar definido y no vacío |
|
||||||
|
* | number | Es numérico |
|
||||||
|
* | int | Es entero |
|
||||||
|
* | float | Es un float |
|
||||||
|
* | bool | Es booleano |
|
||||||
|
* | email | Es un correo |
|
||||||
|
* | enum | Esta en un lista ve valores. Ej: enum:admin,user,guest |
|
||||||
|
* | url | Es una url válida |
|
||||||
|
* |----------+--------------------------------------------------------|
|
||||||
|
*
|
||||||
|
* Las listas de reglas están separadas por |, Ej: required|email
|
||||||
|
*
|
||||||
|
* @author KJ
|
||||||
|
* @website https://kj2.me
|
||||||
|
* @licence MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Libs;
|
||||||
|
|
||||||
|
class Validator {
|
||||||
|
public static string $lastFailed = '';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validar lista de reglas sobre las propiedades de un objeto.
|
||||||
|
*
|
||||||
|
* @param array $rulesList Lista de reglas.
|
||||||
|
* @param Neuron $haystack Objeto al que se le verificarán las reglas.
|
||||||
|
*
|
||||||
|
* @return bool Retorna true solo si todas las reglas se cumplen y false en cuanto una falle.
|
||||||
|
*/
|
||||||
|
public static function validateList(array $rulesList, Neuron $haystack): bool
|
||||||
|
{
|
||||||
|
foreach ($rulesList as $target => $rules) {
|
||||||
|
$rules = preg_split('/\|/', $rules);
|
||||||
|
foreach ($rules as $rule) {
|
||||||
|
if (static::checkRule($haystack->{$target}, $rule))
|
||||||
|
continue;
|
||||||
|
static::$lastFailed = $target.'.'.$rule;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Revisa si una regla se cumple.
|
||||||
|
*
|
||||||
|
* @param mixed $subject Lo que se va a verfificar.
|
||||||
|
* @param string $rule La regla a probar.
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public static function checkRule(mixed $subject, string $rule): bool
|
||||||
|
{
|
||||||
|
$arguments = preg_split('/[:,]/', $rule);
|
||||||
|
$rule = [static::class, $arguments[0]];
|
||||||
|
$arguments[0] = $subject;
|
||||||
|
|
||||||
|
if (is_callable($rule))
|
||||||
|
return call_user_func_array($rule, $arguments);
|
||||||
|
|
||||||
|
throw new \Exception('Bad rule: "'.preg_split('/::/', $rule)[1].'"' );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verifica la regla de manera negativa.
|
||||||
|
*
|
||||||
|
* @param mixed $subject Lo que se va a verfificar.
|
||||||
|
* @param mixed $rule La regla a probar.
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public static function not(mixed $subject, ...$rule): bool
|
||||||
|
{
|
||||||
|
return !static::checkRule($subject, join(':', $rule));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Comprueba que que esté definido/exista.
|
||||||
|
*
|
||||||
|
* @param mixed $subject
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public static function exists(mixed $subject): bool
|
||||||
|
{
|
||||||
|
return isset($subject);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Comprueba que que esté definido y no esté vacío.
|
||||||
|
*
|
||||||
|
* @param mixed $subject
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public static function required(mixed $subject): bool
|
||||||
|
{
|
||||||
|
return isset($subject) && !empty($subject);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* number
|
||||||
|
*
|
||||||
|
* @param mixed $subject
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public static function number(mixed $subject): bool
|
||||||
|
{
|
||||||
|
return is_numeric($subject);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* int
|
||||||
|
*
|
||||||
|
* @param mixed $subject
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public static function int(mixed $subject): bool
|
||||||
|
{
|
||||||
|
return filter_var($subject, FILTER_VALIDATE_INT);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* float
|
||||||
|
*
|
||||||
|
* @param mixed $subject
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public static function float(mixed $subject): bool
|
||||||
|
{
|
||||||
|
return filter_var($subject, FILTER_VALIDATE_FLOAT);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* bool
|
||||||
|
*
|
||||||
|
* @param mixed $subject
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public static function bool(mixed $subject): bool
|
||||||
|
{
|
||||||
|
return filter_var($subject, FILTER_VALIDATE_BOOLEAN);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* email
|
||||||
|
*
|
||||||
|
* @param mixed $subject
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public static function email(mixed $subject): bool
|
||||||
|
{
|
||||||
|
return filter_var($subject, FILTER_VALIDATE_EMAIL);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* url
|
||||||
|
*
|
||||||
|
* @param mixed $subject
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public static function url(mixed $subject): bool
|
||||||
|
{
|
||||||
|
return filter_var($subject, FILTER_VALIDATE_URL);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* enum
|
||||||
|
*
|
||||||
|
* @param mixed $subject
|
||||||
|
* @param mixed $values
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public static function enum(mixed $subject, ...$values): bool
|
||||||
|
{
|
||||||
|
return in_array($subject, $values);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,15 @@
|
||||||
|
<?php
|
||||||
|
namespace Models;
|
||||||
|
|
||||||
|
use Libs\Model;
|
||||||
|
|
||||||
|
class Answer extends Model {
|
||||||
|
public int $option_id;
|
||||||
|
public string $create_at;
|
||||||
|
|
||||||
|
public function add(): void
|
||||||
|
{
|
||||||
|
$this->create_at = date('Y-m-d H:i:s');
|
||||||
|
parent::add();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,65 @@
|
||||||
|
<?php
|
||||||
|
namespace Models;
|
||||||
|
|
||||||
|
use Libs\Crypto;
|
||||||
|
use Libs\Model;
|
||||||
|
|
||||||
|
class Pool extends Model {
|
||||||
|
public string $title;
|
||||||
|
protected array $options;
|
||||||
|
protected int $votes;
|
||||||
|
|
||||||
|
public function votes(): int
|
||||||
|
{
|
||||||
|
if (!isset($this->votes))
|
||||||
|
$this->votes = Answer::where_in('option_id', array_column($this->getOptions(), 'id'))->count();
|
||||||
|
|
||||||
|
return $this->votes;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Devuelve las opciones de la encuesta.
|
||||||
|
*
|
||||||
|
* @return array<PoolOption>
|
||||||
|
*/
|
||||||
|
public function getOptions(): array
|
||||||
|
{
|
||||||
|
if (!isset($this->options))
|
||||||
|
$this->options = PoolOption::where('pool_id', $this->id)->get();
|
||||||
|
|
||||||
|
return $this->options;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Crea la cookie del voto para evitar doble votación.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function createCookie(): void
|
||||||
|
{
|
||||||
|
$cookieValue = Crypto::encrypt64(json_encode([
|
||||||
|
'pid' => $this->id,
|
||||||
|
'exp' => time()+43200
|
||||||
|
]));
|
||||||
|
setcookie('POOL_VOTE', $cookieValue, time()+43200, parse_url(SITE_URL, PHP_URL_PATH));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verifica si ya se ha emitido su voto en la encuesta actual.
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function isVoted(): bool
|
||||||
|
{
|
||||||
|
if (!isset($_COOKIE['POOL_VOTE']))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
$cookie = json_decode(Crypto::decrypt64($_COOKIE['POOL_VOTE']));
|
||||||
|
|
||||||
|
return (json_last_error() == JSON_ERROR_NONE &&
|
||||||
|
$cookie->exp > time() &&
|
||||||
|
$cookie->pid == $this->id);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,18 @@
|
||||||
|
<?php
|
||||||
|
namespace Models;
|
||||||
|
|
||||||
|
use Libs\Model;
|
||||||
|
|
||||||
|
class PoolOption extends Model {
|
||||||
|
public string $value;
|
||||||
|
public int $pool_id;
|
||||||
|
protected int $votes;
|
||||||
|
|
||||||
|
public function votes(): int
|
||||||
|
{
|
||||||
|
if (!isset($this->votes))
|
||||||
|
$this->votes = Answer::where('option_id', $this->id)->count();
|
||||||
|
|
||||||
|
return $this->votes;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,95 @@
|
||||||
|
<?php
|
||||||
|
namespace Models;
|
||||||
|
|
||||||
|
use Libs\Crypto;
|
||||||
|
use Libs\Model;
|
||||||
|
|
||||||
|
class User extends Model {
|
||||||
|
public string $username;
|
||||||
|
protected string $password;
|
||||||
|
|
||||||
|
static protected array $forceSave = ['password'];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Definir la contraseña.
|
||||||
|
*
|
||||||
|
* @param string $password
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function setPassword(string $password): void
|
||||||
|
{
|
||||||
|
$this->password = password_hash($password, PASSWORD_DEFAULT);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Iniciar sesión.
|
||||||
|
*
|
||||||
|
* @param string $username
|
||||||
|
* @param string $password
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public static function login(string $username, string $password): bool
|
||||||
|
{
|
||||||
|
$user = User::where('username', $username)->getFirst();
|
||||||
|
|
||||||
|
if(isset($user) && password_verify($password, $user->password)) {
|
||||||
|
$user->createLoginCookie();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Crea la cookie de sesión.
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function createLoginCookie(): void
|
||||||
|
{
|
||||||
|
setcookie('POOL_LC', $this->getSessionToken(), time()+1209600, parse_url(SITE_URL, PHP_URL_PATH));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cerrar sessión.
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public static function logout(): void {
|
||||||
|
setcookie('POOL_LC', '', 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verifica si el hay un usuario logueado.
|
||||||
|
*
|
||||||
|
* @return static|null
|
||||||
|
*/
|
||||||
|
public static function isLogged(): static|null
|
||||||
|
{
|
||||||
|
if (!isset($_COOKIE['POOL_LC']))
|
||||||
|
return null;
|
||||||
|
|
||||||
|
$cookie = json_decode(Crypto::decrypt64($_COOKIE['POOL_LC']));
|
||||||
|
|
||||||
|
if (json_last_error() == JSON_ERROR_NONE &&
|
||||||
|
isset($cookie->expire) &&
|
||||||
|
$cookie->expire > time())
|
||||||
|
return User::where('username', $cookie->username)->getFirst();
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Devuelve una llave encriptada asociada al correo del usuario.
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function getSessionToken(int $expiration = 1209600): string
|
||||||
|
{
|
||||||
|
return Crypto::encrypt64(
|
||||||
|
json_encode([
|
||||||
|
'username' => $this->username,
|
||||||
|
'expire' => time() + $expiration
|
||||||
|
])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,30 @@
|
||||||
|
<?php
|
||||||
|
namespace Requests;
|
||||||
|
|
||||||
|
use Libs\HTMX;
|
||||||
|
use Libs\Request;
|
||||||
|
|
||||||
|
class LoginRequest extends Request {
|
||||||
|
|
||||||
|
public static function rules(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'username' => 'required',
|
||||||
|
'password' => 'required',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
public static function messages(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'username.required' => 'No se ha definido un usuario.',
|
||||||
|
'password.required' => 'No se ha definido una contraseña.',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function onInvalid(string $error): false
|
||||||
|
{
|
||||||
|
HTMX::retarget('.alert');
|
||||||
|
echo $error;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,33 @@
|
||||||
|
<?php
|
||||||
|
namespace Requests;
|
||||||
|
|
||||||
|
use Libs\HTMX;
|
||||||
|
|
||||||
|
class PoolCreateRequest extends UserRequest {
|
||||||
|
public static function rules(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'title' => 'required',
|
||||||
|
'option1' => 'required',
|
||||||
|
'option2' => 'required',
|
||||||
|
'option3' => 'required',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function messages(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'title.required' => 'No se ha definido un título.',
|
||||||
|
'option1.required' => 'No se ha definido la opción 01.',
|
||||||
|
'option2.required' => 'No se ha definido la opción 02.',
|
||||||
|
'option3.required' => 'No se ha definido la opción 03.',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function onInvalid(string $error): false
|
||||||
|
{
|
||||||
|
HTMX::retarget('.alert');
|
||||||
|
echo $error;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,18 @@
|
||||||
|
<?php
|
||||||
|
namespace Requests;
|
||||||
|
|
||||||
|
use Libs\Request;
|
||||||
|
use Models\Pool;
|
||||||
|
|
||||||
|
class PoolRequest extends Request {
|
||||||
|
public ?Pool $pool;
|
||||||
|
public function validate(): bool
|
||||||
|
{
|
||||||
|
$this->pool = Pool::orderBy('id', 'DESC')->getFirst();
|
||||||
|
|
||||||
|
if (is_null($this->pool))
|
||||||
|
return $this->onInvalid('Aún no hay una encuesta configurada.');
|
||||||
|
|
||||||
|
return parent::validate();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,21 @@
|
||||||
|
<?php
|
||||||
|
namespace Requests;
|
||||||
|
|
||||||
|
use Libs\HTMX;
|
||||||
|
use Libs\Request;
|
||||||
|
use Models\User;
|
||||||
|
|
||||||
|
class UserRequest extends Request {
|
||||||
|
public ?User $user;
|
||||||
|
|
||||||
|
public function validate(): bool
|
||||||
|
{
|
||||||
|
$this->user = User::isLogged();
|
||||||
|
if (is_null($this->user)) {
|
||||||
|
HTMX::redirect('/login');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return parent::validate();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,32 @@
|
||||||
|
<?php
|
||||||
|
namespace Requests;
|
||||||
|
|
||||||
|
use Models\PoolOption;
|
||||||
|
|
||||||
|
class VoteRequest extends PoolRequest {
|
||||||
|
public ?PoolOption $option;
|
||||||
|
|
||||||
|
public function validate(): bool
|
||||||
|
{
|
||||||
|
if (!parent::validate())
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if ($this->pool->id != $this->params->pid)
|
||||||
|
return $this->onInvalid('Voto no válido.');
|
||||||
|
|
||||||
|
$this->option = PoolOption::getById($this->params->vid);
|
||||||
|
if (is_null($this->option) || $this->option->pool_id != $this->params->pid)
|
||||||
|
return $this->onInvalid('Voto no válido.');
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function paramRules(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'pid' => 'int',
|
||||||
|
'vid' => 'int'
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,16 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use Controllers\Pool\HomeController;
|
||||||
|
use Controllers\Pool\PoolCreateController;
|
||||||
|
use Controllers\Pool\PoolFormController;
|
||||||
|
use Controllers\Pool\PoolResultController;
|
||||||
|
use Controllers\Pool\PoolVoteController;
|
||||||
|
use Libs\Router;
|
||||||
|
|
||||||
|
Router::get('/', [HomeController::class, 'handle']);
|
||||||
|
|
||||||
|
Router::get('/config', [PoolFormController::class, 'handle']);
|
||||||
|
Router::post('/config', [PoolCreateController::class, 'handle']);
|
||||||
|
|
||||||
|
Router::post('/pool/{pid}/vote/{vid}', [PoolVoteController::class, 'handle']);
|
||||||
|
Router::get('/results', [PoolResultController::class, 'handle']);
|
|
@ -0,0 +1,8 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use Controllers\User\UserLoginController;
|
||||||
|
use Controllers\User\UserLoginFormController;
|
||||||
|
use Libs\Router;
|
||||||
|
|
||||||
|
Router::get('/login', [UserLoginFormController::class, 'handle']);
|
||||||
|
Router::post('/login', [UserLoginController::class, 'handle']);
|
|
@ -0,0 +1,16 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8"/>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<link href="<?=$this->route('static/css/pico.red.min.css')?>" rel="stylesheet"/>
|
||||||
|
<link href="<?=$this->route('static/css/style.css')?>" rel="stylesheet"/>
|
||||||
|
<title><?=$this->title?></title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<main class="container">
|
||||||
|
<?=$this->content?>
|
||||||
|
</main>
|
||||||
|
<script src="<?=$this->route('static/js/htmx.min.js')?>"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -0,0 +1,38 @@
|
||||||
|
<Layout title="Ingresar">
|
||||||
|
|
||||||
|
<div class="centered-container">
|
||||||
|
<article>
|
||||||
|
<header>
|
||||||
|
<h1 class="center">Iniciar sesión</h1>
|
||||||
|
</header>
|
||||||
|
<form method="POST" hx-boost="true">
|
||||||
|
<hr/>
|
||||||
|
<fieldset>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="username"
|
||||||
|
placeholder="Usuario"
|
||||||
|
autocomplete="off"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
name="password"
|
||||||
|
placeholder="Contraseña"
|
||||||
|
autocomplete="off"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
|
<button type="submit">
|
||||||
|
Enviar
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div
|
||||||
|
hx-on::after-swap="setTimeout(function(){document.querySelector('.alert').innerHTML='' }, 4000)"
|
||||||
|
class="alert center"></div>
|
||||||
|
</form>
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</Layout>
|
|
@ -0,0 +1,23 @@
|
||||||
|
<Layout title="Encuesta">
|
||||||
|
<div class="poll-wrapper">
|
||||||
|
<article class="center">
|
||||||
|
<header>
|
||||||
|
<h1><?=htmlspecialchars($this->pool->title)?></h1>
|
||||||
|
</header>
|
||||||
|
<div class="pool-options">
|
||||||
|
<?php if ($this->pool->isVoted()): ?>
|
||||||
|
¡Gracias por participar!
|
||||||
|
<?php else: ?>
|
||||||
|
<?php foreach($this->pool->getOptions() as $option): ?>
|
||||||
|
<div role="button"
|
||||||
|
hx-target=".pool-options"
|
||||||
|
hx-post="<?=$this->route('/pool/'.$this->pool->id.'/vote/'.$option->id)?>"
|
||||||
|
tabindex="0">
|
||||||
|
<?=htmlspecialchars($option->value)?>
|
||||||
|
</div>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
|
</Layout>
|
|
@ -0,0 +1,55 @@
|
||||||
|
<Layout title="Configuración">
|
||||||
|
<div class="centered-container">
|
||||||
|
<article>
|
||||||
|
<header>
|
||||||
|
<h1 class="center">Configurar nueva encuesta</h1>
|
||||||
|
</header>
|
||||||
|
<form method="POST" hx-boost="true">
|
||||||
|
<hr/>
|
||||||
|
<fieldset>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="title"
|
||||||
|
placeholder="Título de la encuesta"
|
||||||
|
autocomplete="off"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="option1"
|
||||||
|
placeholder="Opción 01"
|
||||||
|
autocomplete="off"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="option2"
|
||||||
|
placeholder="Opción 02"
|
||||||
|
autocomplete="off"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="option3"
|
||||||
|
placeholder="Opción 03"
|
||||||
|
autocomplete="off"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
|
<button type="submit">
|
||||||
|
Crear encuesta
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div
|
||||||
|
hx-on::after-swap="setTimeout(function(){document.querySelector('.alert').innerHTML='' }, 4000)"
|
||||||
|
class="alert center"></div>
|
||||||
|
<?php if ($this->saved): ?>
|
||||||
|
<div class="center autohide">
|
||||||
|
Se ha creado la encuesta!!
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
</form>
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
|
</Layout>
|
|
@ -0,0 +1,37 @@
|
||||||
|
<!-- nothtmx -->
|
||||||
|
<Layout title="Encuesta">
|
||||||
|
<div class="poll-wrapper"
|
||||||
|
hx-trigger="every 1s"
|
||||||
|
hx-get="<?=$this->route('/results')?>">
|
||||||
|
<!-- /nothtmx -->
|
||||||
|
<article class="center">
|
||||||
|
<header>
|
||||||
|
<h1><?=htmlspecialchars($this->pool->title)?></h1>
|
||||||
|
</header>
|
||||||
|
<div class="pool-options">
|
||||||
|
<?php foreach($this->pool->getOptions() as $option): ?>
|
||||||
|
<?php
|
||||||
|
if ($option->votes() == 0)
|
||||||
|
$percent = 0;
|
||||||
|
else
|
||||||
|
$percent = round(($option->votes() * 100 / $this->pool->votes()), 2);
|
||||||
|
?>
|
||||||
|
<label>
|
||||||
|
<strong>
|
||||||
|
<?=htmlspecialchars($option->value)?>:
|
||||||
|
</strong>
|
||||||
|
<?=$percent?>% (<?=$option->votes()?> votos)
|
||||||
|
<progress value="<?=$percent?>" max="100"/>
|
||||||
|
</label>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</div>
|
||||||
|
<footer>
|
||||||
|
<strong>
|
||||||
|
Total de votos: <?=$this->pool->votes()?>
|
||||||
|
</strong>
|
||||||
|
</footer>
|
||||||
|
</article>
|
||||||
|
<!-- nothtmx -->
|
||||||
|
</div>
|
||||||
|
</Layout>
|
||||||
|
<!-- /nothtmx -->
|
Loading…
Reference in New Issue