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