Compare commits

...

40 Commits

Author SHA1 Message Date
kj
e9126e7cde Fix: Implicitly marking parameter as nullable is deprecated.
PHP 8.4 deprecation.
2025-06-07 14:27:47 -03:00
kj
7169d2cae3 Fix: render is not using the defined extension. 2025-06-07 14:17:11 -03:00
kj
66b2bc0d91 Remove unnecesary php close. 2025-06-07 14:14:16 -03:00
kj
c8ab2aa2cc Remove unnecesary echo. 2025-05-20 12:49:20 -03:00
kj
1e302a9ea7 BREAKING CHANGE: Change unnecesary false return type. 2025-04-19 15:44:16 -03:00
kj
d0d0d4dc76 Verify if a valid http query string after run parse_str. 2025-02-20 08:22:47 -03:00
kj
595e9c1316 Save body request as a property. 2025-02-20 08:22:37 -03:00
kj
45abea5301 Add delete request params. 2025-02-20 06:28:03 -03:00
kj
d441f001ec Add type of items on array on dockblock for "all" method. 2025-02-03 16:04:39 -03:00
kj
19da122e05 Add type of items on array on dockblock for get method. 2025-02-03 16:02:27 -03:00
KJ
1a0164c8ed Change static methods to non-static and made onInvalid public. 2024-10-30 11:53:44 -04:00
KJ
ad9f8ec67d Remove unnecesary brackets. 2024-10-29 19:12:25 -04:00
KJ
31c5c63952 Remove innecesary return. 2024-10-29 19:10:47 -04:00
KJ
6aef212350 Fix className not returning the classname in the right format. 2024-10-25 10:40:57 -04:00
KJ
c600688725 Improve return array dockblocks. 2024-09-23 18:09:38 -04:00
KJ
3e27b1b7af Allow null on enum properties. 2024-09-23 15:06:44 -04:00
KJ
73b7b8f72a Change required valitator to not allow empty values and add exists.
The exists validator do the same as the old required.
2024-09-18 14:33:33 -04:00
KJ
7baad428ec Refactor request library. 2024-09-08 14:43:56 -04:00
KJ
3d2a607768 Fix where_in is wiping previous where/and/or.
For now, works as an AND, but maybe later, same as where will exists
new methods: AndIn and OrIN.
2024-08-30 16:26:03 -04:00
KJ
df424ffab5 Model properties now can be typed as enums.
With this PHP 8.0 support is dropped.
2024-08-27 19:01:02 -04:00
KJ
daf7250882 Catch and verify put and patch input values. 2024-08-13 10:22:44 -04:00
KJ
05cd83fd10 Remove unused variable. 2024-07-31 03:29:49 -04:00
KJ
6b470a181d Fix: Remove unnecesary parameter. 2024-07-10 09:06:51 -04:00
KJ
7beb161d2b Ensure db is in transaction to commit or rollback. 2024-06-04 07:18:30 -04:00
KJ
701caae7eb Change route method to static. 2024-05-29 13:24:20 -04:00
KJ
100bdfe006 Change private method to protected instead. 2024-05-28 22:27:20 -04:00
KJ
f1b79fdbc0 Add http code 422 on verification failed. 2024-05-25 17:41:33 -04:00
KJ
406f9a10a1 Add head comment. 2024-05-25 17:19:08 -04:00
KJ
cc3cb6be41 Fix: a return was forgot. 2024-05-25 17:11:07 -04:00
KJ
59fff2a586 Add validation on Request. 2024-05-25 16:59:59 -04:00
KJ
cd1685d2e7 fix on a docblock. 2024-05-21 15:06:02 -04:00
KJ
b85fb7e034 Allow configure SITE_URL with or without slash at end. 2024-05-16 13:48:49 -04:00
KJ
a10308a8f6 Fix route and redirect methods error when path not start with slash. 2024-05-16 13:20:35 -04:00
KJ
9a1e5a2379 Add some explanatory comments to config. 2024-05-14 02:54:23 -04:00
KJ
fa60ec5bb4 Move constant definition to config.php 2024-05-14 02:53:39 -04:00
KJ
b294b1d627 Update readme.
Fix description and add links to starting manual.
2024-05-13 01:08:44 -04:00
KJ
27b5ed6015 Convert model name from PascalCase to snake_case (table name). 2024-05-12 04:38:45 -04:00
KJ
f9b2c678f7 Support multiple databases at same time. 2024-05-12 03:47:09 -04:00
KJ
e294eaa7af Add route function for View objects. 2024-05-09 23:12:59 -04:00
KJ
186d640c8f fix: an empty SITE_URL return a incorrect value of basePath. 2024-05-09 23:10:58 -04:00
10 changed files with 627 additions and 277 deletions

View File

@ -1,11 +1,14 @@
<?php
// Configuración de la base de datos
define('DB_TYPE', 'mysql');
define('DB_HOST', 'localhost');
define('DB_NAME', '');
define('DB_USER', '');
define('DB_PASS', '');
//define('SITE_URL', '');
// Configuración del sitio
define('SITE_URL', '');
// Configuración avanzada
define('ROOT_DIR', __DIR__);
?>
define('ROOT_CORE', ROOT_DIR.'/src');

View File

@ -1,26 +1,20 @@
<?php
require_once('config.php');
define('ROOT_CORE', ROOT_DIR.'/src');
// 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)) {
if (file_exists($file))
require_once $file;
return;
}
});
// Incluir routers
$routers = glob(ROOT_CORE.'/Routers/*.php');
foreach($routers as $file){
foreach($routers as $file)
require_once($file);
}
\Libs\Router::apply();
?>

View File

@ -1,20 +1,106 @@
* DuckBrain - PHP Microframework
#+TITLE: Duckbrain
#+AUTHOR: KJ
#+OPTIONS: toc:nil
Este microframework PHP tiene el objetivo de presentar un framework sencillo y potente que sea válido especialmente para proyectos pequeños o simples, sin limitar demasiado, ni depender de cosas que agranden innecesariamente proyectos.
Este conjunto de librerías (microframework, si así lo prefieres) tiene el objetivo de presentar una herramienta de trabajo sencilla y potente que sea válida especialmente para proyectos pequeños o simples, sin limitar, ni depender de cosas que agranden innecesariamente dichos proyectos.
Esto no es un intento de mi parte de reinventar la rueda, de hecho los primeros commits los hice con la intención de que solo fuera una prueba de concepto, pero me gustó como quedó y decidí darle continuidad hasta hacerlo usable para mi mismo.
Tuve la idea de hacer esta herramienta luego de ver como proyectos relativamente pequeños eran hechos con frameworks demasiado grandes para la envergadura de dichos proyectos. No es bueno matar moscas a cañonazos.
Tuve la idea de hacer este conjunto de librerías/microframework luego de ver como proyectos relativamente pequeños eran hechos con frameworks demasiado pesados para lo que eran.
Del mismo modo revisé algunos microframeworks y si bien me parecen una buena opción, aún me quedaba la espinita de que seguían siendo muy complejos para que un programador novato pudiera leer su código, entenderlo y modificarlo, así que intenté hacer algo más simple
También revisé otros microframeworks y si bien me parecen una buena opción para hacer los trabajos, aún me quedaba la espinita de que seguían siendo muy grandes para que un programador novato pudiera leerlo, entenderlo y modificarlo, así que intenté hacer algo más simple aún y de ahí salió este proyecto.
Lo ideal sería mantener el código sencillo, lo suficiente como para que cualquier novato que sepa POO y PHP pueda leerlo rápido, entenderlo y modificarlo a gusto. Por este motivo también he decidido desligarlo en lo posible de composer o cualquier cosa similar, ya que no pocos programadores en etapa de aprendizaje al encontrarse con frameworks más complicados o con herramientas como composer, terminan trabajando con cosas que no comprenden ni pueden arreglar por si mismos en caso de fallo, ya que son completamente dependientes de dichas herramientas.
El código no es perfecto, pero lo iré perfeccionando, ya que es algo que estoy usando actualmente para mis trabajos, de modo que puedo ir viendo que cosas se pueden ir mejorando.
Lo que busco es mantener un código sencillo, lo suficiente como para que cualquier novato que sepa POO y PHP pueda leer su código rápidamente, entenderlo y modificarlo a gusto. Por este motivo también he decidido desligarlo en lo posible de composer o cualquier cosa similar, ya que no pocos programadores en etapa de aprendizaje al encontrarse con frameworks más complicados o con herramientas como composer, terminan trabajando con cosas que no comprenden ni pueden arreglar por si mismos en caso de fallo, llevándolos a la dependencia total de dichas herramientas.
* Uso / Documentación
Queda pendiente, si quieres usarlo ya mismo, puedes leer los comentarios que he colocado en el código o usar [[https://www.phpdoc.org/][phpdoc]] para generarla a partir de los bloques de comentarios.
Actualmente existe un manual de inicio que puedes leerlo desde [[https://tools.kj2.me/duckbrain/starting-manual.html][aquí]] o desde [[https://git.kj2.me/kj/duckbrain-docs/src/branch/master/starting-manual.org][aquí]].
Si quieres leer el manual de inicio en su formato original (necesitas un editor/lector que soporte org-mode) puedes descargarla desde [[https://tools.kj2.me/duckbrain/starting-manual.org][aquí]].
Para el resto de la documentación, el código usa [[https://www.phpdoc.org/][phpdoc]], por lo que puedes tener toda la documentación API desde tu IDE o Editor de texto preferido siempre que soporte dicha característica. O si lo prefieres, puedes generarla en unbonito htm local usando la herramienta [[https://docs.phpdoc.org/3.0/packages/phpDocumentor.html][phpDocumentor]].
* Soporte de gestores de bases de datos
En la siguiente tabla se encuentra la lista de estados de los gestores de bases de datos que he probado o que planeo probar para asegurarme de que sean realmente compatibles.
*Entiéndase*:
+ *ok* como que ha sido probado y funciona.
+ *En blanco* como que no ha sido probado aún.
+ *error* como que fue probado, no funciona y no ha sido aún arreglado.
+ *not supported* como no soportado por el gestor de bases de datos.
+ *fixed* para aquello que no existe en el gestor de DB, pero la librería lo traduce a un equivalente.
|------------------+---------------+---------------+------------|
| method | MySQL/MariaDB | sqlite3 | postgreSQL |
|------------------+---------------+---------------+------------|
| db | ok | ok | |
|------------------+---------------+---------------+------------|
| query | ok | ok | |
|------------------+---------------+---------------+------------|
| resetQuery | ok | ok | |
|------------------+---------------+---------------+------------|
| buildQuery | ok | ok | |
|------------------+---------------+---------------+------------|
| getInstance | ok | ok | |
|------------------+---------------+---------------+------------|
| getVars | ok | ok | |
|------------------+---------------+---------------+------------|
| className | ok | ok | |
|------------------+---------------+---------------+------------|
| table | ok | ok | |
|------------------+---------------+---------------+------------|
| update | ok | ok | |
|------------------+---------------+---------------+------------|
| beginTransaction | ok | ok | |
|------------------+---------------+---------------+------------|
| rollBack | ok | ok | |
|------------------+---------------+---------------+------------|
| commit | ok | ok | |
|------------------+---------------+---------------+------------|
| add | ok | ok | |
|------------------+---------------+---------------+------------|
| save | ok | ok | |
|------------------+---------------+---------------+------------|
| delete | ok | ok | |
|------------------+---------------+---------------+------------|
| select | ok | ok | |
|------------------+---------------+---------------+------------|
| from | ok | ok | |
|------------------+---------------+---------------+------------|
| where | ok | ok | |
|------------------+---------------+---------------+------------|
| where_in | ok | ok | |
|------------------+---------------+---------------+------------|
| leftJoin | ok | ok | |
|------------------+---------------+---------------+------------|
| rightJoin | ok | not supported | |
|------------------+---------------+---------------+------------|
| innerJoin | ok | ok | |
|------------------+---------------+---------------+------------|
| and | ok | ok | |
|------------------+---------------+---------------+------------|
| or | ok | ok | |
|------------------+---------------+---------------+------------|
| groupBy | ok | ok | |
|------------------+---------------+---------------+------------|
| limit | ok | ok | |
|------------------+---------------+---------------+------------|
| orderBy | ok | ok | |
|------------------+---------------+---------------+------------|
| count | ok | ok | |
|------------------+---------------+---------------+------------|
| getById | ok | ok | |
|------------------+---------------+---------------+------------|
| search | ok | ok | |
|------------------+---------------+---------------+------------|
| get | ok | ok | |
|------------------+---------------+---------------+------------|
| getFirst | ok | ok | |
|------------------+---------------+---------------+------------|
| all | ok | ok | |
|------------------+---------------+---------------+------------|
| setNull | ok | ok | |
|------------------+---------------+---------------+------------|
* Contacto

View File

@ -1,95 +0,0 @@
* Sobre la actualización
Previamente, el sistema usaba [[https://www.php.net/mysqli][MySQLi]] para el Modelo/ORM, como actualización, se ha cambiado para funcionar con [[https://www.php.net/pdo][PDO]].
* Notas de migración
Estos son detalles importantes a tomar en cuenta a la hora de migrar del anterior ModelMySQL (MySQLi) al actual Model (PDO).
+ El modelo base ha cambiado de nombre de =ModelMySQL= a =Model=.
+ El método =query= ahora devuelve un =array= en lugar de un =mysqli_result=.
+ Se han depurado los métodos =sql_calc_found_rows= y =found_rows=.
+ Se ha cambiado =dbname=, =dbuser= y =dbpass= a =DB_NAME=, =DB_USER= y =DB_PASS=, respectivamente.
+ Se ha añadido la necesitad de la constante =DB_TYPE= para indicar el driver PDO a usar (ej. mysql, sqlite).
* Pruebas
En la siguiente tabla se encuentra la lista de estados de los SGBD que he probado (MySQL/MariaDB) o que planeo probar (sqlite3, postgreSQL) para asegurarme de que sean realmente compatibles.
*Entiéndase*:
+ *ok* como que ha sido probado y funciona.
+ En blanco como que no ha sido probado aún.
+ *error* como que fue probado, no funciona y no ha sido aún arreglado.
+ *not supported* como no soportado por el SGBD.
+ *fixed* para aquello que no existe, pero la librería lo traduce a un equivalente.
|------------------+---------------+---------------+------------|
| method | MySQL/MariaDB | sqlite3 | postgreSQL |
|------------------+---------------+---------------+------------|
| db | ok | ok | |
|------------------+---------------+---------------+------------|
| query | ok | ok | |
|------------------+---------------+---------------+------------|
| resetQuery | ok | ok | |
|------------------+---------------+---------------+------------|
| buildQuery | ok | ok | |
|------------------+---------------+---------------+------------|
| getInstance | ok | ok | |
|------------------+---------------+---------------+------------|
| getVars | ok | ok | |
|------------------+---------------+---------------+------------|
| className | ok | ok | |
|------------------+---------------+---------------+------------|
| table | ok | ok | |
|------------------+---------------+---------------+------------|
| update | ok | ok | |
|------------------+---------------+---------------+------------|
| beginTransaction | ok | ok | |
|------------------+---------------+---------------+------------|
| rollBack | ok | ok | |
|------------------+---------------+---------------+------------|
| commit | ok | ok | |
|------------------+---------------+---------------+------------|
| add | ok | ok | |
|------------------+---------------+---------------+------------|
| save | ok | ok | |
|------------------+---------------+---------------+------------|
| delete | ok | ok | |
|------------------+---------------+---------------+------------|
| select | ok | ok | |
|------------------+---------------+---------------+------------|
| from | ok | ok | |
|------------------+---------------+---------------+------------|
| where | ok | ok | |
|------------------+---------------+---------------+------------|
| where_in | ok | ok | |
|------------------+---------------+---------------+------------|
| leftJoin | ok | ok | |
|------------------+---------------+---------------+------------|
| rightJoin | ok | not supported | |
|------------------+---------------+---------------+------------|
| innerJoin | ok | ok | |
|------------------+---------------+---------------+------------|
| and | ok | ok | |
|------------------+---------------+---------------+------------|
| or | ok | ok | |
|------------------+---------------+---------------+------------|
| groupBy | ok | ok | |
|------------------+---------------+---------------+------------|
| limit | ok | ok | |
|------------------+---------------+---------------+------------|
| orderBy | ok | ok | |
|------------------+---------------+---------------+------------|
| count | ok | ok | |
|------------------+---------------+---------------+------------|
| getById | ok | ok | |
|------------------+---------------+---------------+------------|
| search | ok | ok | |
|------------------+---------------+---------------+------------|
| get | ok | ok | |
|------------------+---------------+---------------+------------|
| getFirst | ok | ok | |
|------------------+---------------+---------------+------------|
| all | ok | ok | |
|------------------+---------------+---------------+------------|
| setNull | ok | ok | |
|------------------+---------------+---------------+------------|

View File

@ -3,11 +3,6 @@
* Database - DuckBrain
*
* Clase diseñada para crear y devolver una única instancia PDO (database).
* Hace uso de las siguientes constantes:
* DB_TYPE, DB_NAME, DB_HOST, DB_USER, DB_PASS
*
* Si DB_TYPE es sqlite, usará DB_NAME como el nombre del archivo sqlite.
* Además DB_USER y DB_PASS, no será necesariop que estén definidos.
*
* @author KJ
* @website https://kj2.me
@ -21,7 +16,7 @@ use PDOException;
use Exception;
class Database extends PDO {
static private ?PDO $db = null;
static private array $databases = [];
private function __construct() {}
@ -30,29 +25,34 @@ class Database extends PDO {
*
* @return PDO
*/
static public function getInstance(): PDO
static public function getInstance(
string $type = 'mysql',
string $host = 'localhost',
string $name = '',
string $user = '',
string $pass = '',
): PDO
{
if (is_null(self::$db)) {
$key = $type.'/'.$host.'/'.$name.'/'.$user;
if (empty(static::$databases[$key])) {
if (DB_TYPE == 'sqlite') {
$dsn = DB_TYPE .':'. DB_NAME;
!defined('DB_USER') && define('DB_USER', '');
!defined('DB_PASS') && define('DB_PASS', '');
if ($type == 'sqlite') {
$dsn = $type .':'. $name;
} else
$dsn = DB_TYPE.':dbname='.DB_NAME.';host='.DB_HOST;
$dsn = $type.':dbname='.$name.';host='.$host;
try {
self::$db = new PDO($dsn, DB_USER, DB_PASS);
static::$databases[$key] = new PDO($dsn, $user, $pass);
} catch (PDOException $e) {
throw new Exception(
'Error at connect to database: ' . $e->getMessage()
);
}
self::$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
self::$db->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC);
static::$databases[$key]->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
static::$databases[$key]->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC);
}
return self::$db;
return static::$databases[$key];
}
}
?>

View File

@ -3,7 +3,8 @@
* Model - DuckBrain
*
* Modelo ORM para objetos que hagan uso de una base de datos.
* Depende de Libs\Database.
* 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
@ -30,7 +31,6 @@ class Model {
static protected array $forceSave = [];
static protected string $table;
static protected string $tableSufix = 's';
static protected ?PDO $db = null;
static protected array $queryVars = [];
static protected array $querySelect = [
'select' => ['*'],
@ -51,10 +51,19 @@ class Model {
*/
protected static function db(): PDO
{
if (is_null(static::$db))
static::$db = Database::getInstance();
return static::$db;
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
);
}
/**
@ -76,7 +85,10 @@ class Model {
*/
public function rollBack(): bool
{
return static::db()->rollBack();
if ( static::db()->inTransaction())
return static::db()->rollBack();
else
return true;
}
/**
@ -87,7 +99,10 @@ class Model {
*/
public function commit(): bool
{
return static::db()->commit();
if (static::db()->inTransaction())
return static::db()->commit();
else
return true;
}
/**
@ -120,7 +135,6 @@ class Model {
$vars = json_encode(static::$queryVars);
echo "<pre>";
throw new Exception(
"\nError at query to database.\n" .
"Query: $query\n" .
@ -227,11 +241,18 @@ class Model {
*/
protected static function getInstance(array $elem = []): static
{
$class = get_called_class();
$instance = new $class;
$class = get_called_class();
$instance = new $class;
$reflection = new ReflectionClass($instance);
$properties = $reflection->getProperties();
$propertyNames = array_column($properties, 'name');
foreach ($elem as $key => $value) {
$instance->$key = $value;
$index = array_search($key, $propertyNames);
if (is_numeric($index) && enum_exists($properties[$index]->getType()->getName()))
$instance->$key = $properties[$index]->getType()->getName()::tryfrom($value);
else
$instance->$key = $value;
}
return $instance;
@ -263,10 +284,14 @@ class Model {
$result[$value] = isset($this->$value)
? $this->$value: null;
foreach ($result as $i => $property)
foreach ($result as $i => $property) {
if (gettype($property) == 'boolean')
$result[$i] = $property ? '1' : '0';
if ($property instanceof \UnitEnum)
$result[$i] = $property->value;
}
return $result;
}
@ -278,7 +303,9 @@ class Model {
*/
public static function className(): string
{
return strtolower(substr(strrchr(get_called_class(), '\\'), 1));
return substr(
strrchr(get_called_class(), '\\'), 1
);
}
/**
@ -292,7 +319,13 @@ class Model {
{
if (isset(static::$table))
return static::$table;
return static::className().static::$tableSufix;
return strtolower(
preg_replace(
'/(?<!^)[A-Z]/', '_$0',
static::className()
)
).static::$tableSufix;
}
/**
@ -416,7 +449,7 @@ class Model {
* @param string $operatorOrValue
* El operador o el valor a comparar como igual en caso de que $value no se defina.
*
* @param string $value
* @param string|null $value
* (opcional) El valor a comparar en la columna.
*
* @param bool $no_filter
@ -426,10 +459,10 @@ class Model {
* @return static
*/
public static function where(
string $column,
string $operatorOrValue,
string $value = null,
bool $no_filter = false
string $column,
string $operatorOrValue,
?string $value = null,
bool $no_filter = false
): static
{
return static::and(
@ -449,7 +482,7 @@ class Model {
* @param string $operatorOrValue
* El operador o el valor a comparar como igual en caso de que $value no se defina.
*
* @param string $value
* @param string|null $value
* (opcional) El valor el valor a comparar en la columna.
*
* @param bool $no_filter
@ -459,10 +492,10 @@ class Model {
* @return static
*/
public static function and(
string $column,
string $operatorOrValue,
string $value = null,
bool $no_filter = false
string $column,
string $operatorOrValue,
?string $value = null,
bool $no_filter = false
): static
{
if (is_null($value)) {
@ -490,7 +523,7 @@ class Model {
* @param string $operatorOrValue
* El operador o el valor a comparar como igual en caso de que $value no se defina.
*
* @param string $value
* @param string|null $value
* (opcional) El valor el valor a comparar en la columna.
*
* @param bool $no_filter
@ -500,10 +533,10 @@ class Model {
* @return static
*/
public static function or(
string $column,
string $operatorOrValue,
string $value = null,
bool $no_filter = false
string $column,
string $operatorOrValue,
?string $value = null,
bool $no_filter = false
): static
{
if (is_null($value)) {
@ -548,9 +581,14 @@ class Model {
}
if ($in)
static::$querySelect['where'] = "$column IN (".join(', ', $arrIn).")";
$where_in = "$column IN (".join(', ', $arrIn).")";
else
static::$querySelect['where'] = "$column NOT IN (".join(', ', $arrIn).")";
$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();
}
@ -567,16 +605,16 @@ class Model {
* @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
* @param string|null $columnB
* (opcional) Columna a comparar para hacer el join.
*
* @return static
*/
public static function leftJoin(
string $table,
string $columnA,
string $operatorOrColumnB,
string $columnB = null
string $table,
string $columnA,
string $operatorOrColumnB,
?string $columnB = null
): static
{
if (is_null($columnB)) {
@ -601,16 +639,16 @@ class Model {
* @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
* @param string|null $columnB
* (opcional) Columna a comparar para hacer el join.
*
* @return static
*/
public static function rightJoin(
string $table,
string $columnA,
string $operatorOrColumnB,
string $columnB = null
string $table,
string $columnA,
string $operatorOrColumnB,
?string $columnB = null
): static
{
if (is_null($columnB)) {
@ -635,16 +673,16 @@ class Model {
* @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
* @param string|null $columnB
* (opcional) Columna a comparar para hacer el join.
*
* @return static
*/
public static function innerJoin(
string $table,
string $columnA,
string $operatorOrColumnB,
string $columnB = null
string $table,
string $columnA,
string $operatorOrColumnB,
?string $columnB = null
): static
{
if (is_null($columnB)) {
@ -785,20 +823,18 @@ class Model {
* @param string $search
* Contenido a buscar.
*
* @param array $in
* @param array|null $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
public static function search(string $search, ?array $in = null): static
{
if ($in == null) {
$className = get_called_class();
$in = array_keys((new $className())->getVars());
}
$db = static::db();
$search = static::bindValue($search);
$where = [];
@ -824,7 +860,7 @@ class Model {
* @param bool $resetQuery
* (opcional) Indica si el query debe reiniciarse o no (por defecto es true).
*
* @return array
* @return array<static>
* Arreglo con instancias del la clase actual resultantes del query.
*/
public static function get(bool $resetQuery = true): array
@ -848,7 +884,7 @@ class Model {
* (opcional) Indica si el query debe reiniciarse o no (por defecto es true).
*
* @return static|null
* Puede retornar un objeto static o null.
* Puede retornar una instancia de la clase actual o null.
*/
public static function getFirst(bool $resetQuery = true): ?static
{
@ -860,7 +896,7 @@ class Model {
/**
* Obtener todos los elementos del la tabla de la instancia actual.
*
* @return array
* @return array<static>
* Contiene un arreglo de instancias de la clase actual.
*/
public static function all(): array

View File

@ -13,46 +13,137 @@
namespace Libs;
class Request extends Neuron {
/**
* @var Neuron $get Objeto con todos los valores de $_GET.
*/
public Neuron $get;
/**
* @var Neuron $post Objeto con todos los valores de $_POST.
*/
public Neuron $post;
/**
* @var Neuron $json Objeto con todos los valores json enviados.
*/
public Neuron $put;
public Neuron $patch;
public Neuron $delete;
public Neuron $json;
/**
* @var mixed $params Objeto con todos los valores pseudovariables de la uri.
*/
public Neuron $params;
/**
* @var mixed $path Ruta actual tomando como raíz la instalación de DuckBrain.
*/
public string $path;
public string $error;
public string $body;
public array $next;
/**
* __construct
*
* @param string $path Ruta actual tomando como raíz la instalación de DuckBrain.
*/
public function __construct(string $path = '/')
public function __construct()
{
$this->path = $path;
$this->path = Router::currentPath();
$this->get = new Neuron($_GET);
$this->post = new Neuron($_POST);
$this->put = new Neuron();
$this->patch = new Neuron();
$this->delete = new Neuron();
$this->body = file_get_contents("php://input");
$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)
(object) json_decode(trim($this->body), false)
);
else
$this->json = new Neuron();
else {
$this->json = new Neuron();
if (in_array($_SERVER['REQUEST_METHOD'], ['PUT', 'PATCH', 'DELETE']) &&
preg_match('/^[^;?\/:@&=+$,]{1,255}[=]/', $this->body, $matches)) { // Con la expresión regular verificamos que sea un http query string válido y evitamos errores de memoria en caso de que el body tenga algo más grande que eso.
parse_str($this->body, $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']) {
'POST', 'PUT', 'PATCH', 'DELETE' => $this->{strtolower($_SERVER['REQUEST_METHOD'])},
default => $this->get
};
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);
}
static::onInvalid($error);
return false;
}
/**
* Reglas para el método actual.
*
* @return array
*/
public function rules(): array {
return [];
}
/**
* Reglas para los parámetros por URL.
*
* @return array
*/
public function paramRules(): array {
return [];
}
/**
* Reglas para los parámetros GET.
*
* @return array
*/
public function getRules(): array {
return [];
}
/**
* Mensajes de error en caso de fallar una validación.
*
* @return array
*/
public function messages(): array {
return [];
}
/**
* Función a ejecutar cuando se ha detectado un valor no válido.
*
* @param string $error
*
* @return void
*/
public function onInvalid(string $error): void
{
http_response_code(422);
print($error);
}
}

View File

@ -82,8 +82,8 @@ class Router {
*/
public static function basePath(): string
{
if (defined('SITE_URL'))
return parse_url(SITE_URL, PHP_URL_PATH);
if (defined('SITE_URL') && !empty(SITE_URL))
return rtrim(parse_url(SITE_URL, PHP_URL_PATH), '/').'/';
return str_replace($_SERVER['DOCUMENT_ROOT'], '/', ROOT_DIR);
}
@ -100,7 +100,7 @@ class Router {
*/
public static function redirect(string $path): void
{
header('Location: '.static::basePath().substr($path,1));
header('Location: '.static::basePath().ltrim($path, '/'));
exit;
}
@ -114,7 +114,7 @@ class Router {
* @return static
* Devuelve la instancia actual.
*/
public static function middleware(callable $callback, int $priority = null): static
public static function middleware(callable $callback, ?int $priority = null): static
{
if (!isset(static::$last))
return new static();
@ -202,13 +202,13 @@ class Router {
*
* @param string $path
* Ruta con pseudovariables.
* @param callable $callback
* @param callable|null $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
public static function get(string $path, ?callable $callback = null): static
{
return static::configure('get', $path, $callback);
}
@ -218,13 +218,13 @@ class Router {
*
* @param string $path
* Ruta con pseudovariables.
* @param callable $callback
* @param callable|null $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
public static function post(string $path, ?callable $callback = null): static
{
return static::configure('post', $path, $callback);
}
@ -234,14 +234,14 @@ class Router {
*
* @param string $path
* Ruta con pseudovariables.
* @param callable $callback
* @param callable|null $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
public static function put(string $path, ?callable $callback = null): static
{
return static::configure('put', $path, $callback);
}
@ -251,13 +251,13 @@ class Router {
*
* @param string $path
* Ruta con pseudovariables.
* @param callable $callback
* @param callable|null $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
public static function patch(string $path, ?callable $callback = null): static
{
return static::configure('patch', $path, $callback);
}
@ -267,13 +267,13 @@ class Router {
*
* @param string $path
* Ruta con pseudovariables
* @param callable $callback
* @param callable|null $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
public static function delete(string $path, ?callable $callback = null): static
{
return static::configure('delete', $path, $callback);
}
@ -290,29 +290,16 @@ class Router {
}
/**
* Aplica los routers.
* Aplica la configuración de rutas.
*
* Este método ha de ser llamado luego de que todos los routers hayan sido configurados.
* @param string|null $path (opcional) Ruta a usar. Si no se define, detecta la ruta actual.
*
* En caso que la ruta actual coincida con un router configurado, se comprueba si hay middleware; Si hay
* middleware, se enviará el callback y los datos de la petición como un Neuron. Caso contrario, se enviarán
* los datos directamente al callback.
*
* Con middleware:
* $middleware($callback, $req)
*
* Sin middleware:
* $callback($req)
*
* $req es una instancia de Neuron que tiene los datos de la petición.
*
* Si no la ruta no coincide con ninguna de las rutas configuradas, ejecutará el callback $notFoundCallback
* @return void
*/
public static function apply(): void
public static function apply(?string $path = null): void
{
$path = static::currentPath();
$routers = match($_SERVER['REQUEST_METHOD']) { // Según el método selecciona un arreglo de routers configurados
$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,
@ -320,25 +307,58 @@ class Router {
default => static::$get
};
$req = new Request(static::currentPath());
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]);
// Comprobando pseudo variables en la ruta
// 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];
$req->params->$paramName = urldecode($match[0]);
$paramName = $router['paramNames'][$index-1];
$request->params->$paramName = urldecode($match[0]);
}
}
// Llamar al último callback configurado
$next = array_pop($router['callback']);
$req->next = $router['callback'];
$data = call_user_func_array($next, [$req]);
// 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));
@ -349,7 +369,6 @@ class Router {
}
// Si no hay router que coincida llamamos a $notFoundCallBack
call_user_func_array(static::$notFoundCallback, [$req]);
call_user_func_array(static::$notFoundCallback, [new Request]);
}
}
?>

201
src/Libs/Validator.php Normal file
View File

@ -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);
}
}

View File

@ -16,16 +16,16 @@ class View extends Neuron {
/**
* Incluye el archivo.
*
* @param string $viewName Ruta relativa y el nommbre sin extensión del archivo.
* @param string $viewPath (opcional) Ruta donde se encuentra la vista.
* @param string $extension (opcional) Extensión del archivo.
* @param string $viewName Ruta relativa y el nommbre sin extensión del archivo.
* @param string|null $viewPath (opcional) Ruta donde se encuentra la vista.
* @param string $extension (opcional) Extensión del archivo.
*
* @return void
*/
private function include(
string $viewName,
string $viewPath = null,
string $extension = 'php'
protected function include(
string $viewName,
?string $viewPath = null,
string $extension = 'php'
): void
{
$view = $this;
@ -44,7 +44,7 @@ class View extends Neuron {
*
* @param string $viewName Ruta relativa y el nommbre sin extensión del archivo.
* @param array|Neuron $params (opcional) Arreglo que podrá ser usado en la vista mediante $view ($param['index'] se usaría así: $view->index)
* @param string $viewPath (opcional) Ruta donde se encuentra la vista. En caso de que la vista no se encuentre en esa ruta, se usará la ruta por defecto "src/Views/".
* @param string|null $viewPath (opcional) Ruta donde se encuentra la vista. En caso de que la vista no se encuentre en esa ruta, se usará la ruta por defecto "src/Views/".
* @param string $extension (opcional) Extensión del archivo.
*
* @return void
@ -52,27 +52,27 @@ class View extends Neuron {
public static function render(
string $viewName,
array|Neuron $params = [],
string $viewPath = null,
?string $viewPath = null,
string $extension = 'php'
): void
{
$instance = new View($params);
$instance->html($viewName, $viewPath);
$instance->html($viewName, $viewPath, $extension);
}
/**
* Renderiza las vistas HTML
*
* @param string $viewName Ruta relativa y el nommbre sin extensión del archivo ubicado en src/Views
* @param string $viewPath (opcional) Ruta donde se encuentra la vista. En caso de que la vista no se encuentre en esa ruta, se usará la ruta por defecto "src/Views/".
* @param string $extension (opcional) Extensión del archivo.
* @param string $viewName Ruta relativa y el nommbre sin extensión del archivo ubicado en src/Views
* @param string|null $viewPath (opcional) Ruta donde se encuentra la vista. En caso de que la vista no se encuentre en esa ruta, se usará la ruta por defecto "src/Views/".
* @param string $extension (opcional) Extensión del archivo.
*
* @return void
*/
public function html(
string $viewName,
string $viewPath = null,
string $extension = 'php'
string $viewName,
?string $viewPath = null,
string $extension = 'php'
): void
{
$this->include(
@ -85,16 +85,16 @@ class View extends Neuron {
/**
* Renderiza código CSS.
*
* @param string $viewName Ruta relativa y el nommbre sin extensión del archivo ubicado en src/Views
* @param string $viewPath (opcional) Ruta donde se encuentra la vista. En caso de que la vista no se encuentre en esa ruta, se usará la ruta por defecto "src/Views/".
* @param string $extension (opcional) Extensión del archivo.
* @param string $viewName Ruta relativa y el nommbre sin extensión del archivo ubicado en src/Views
* @param string|null $viewPath (opcional) Ruta donde se encuentra la vista. En caso de que la vista no se encuentre en esa ruta, se usará la ruta por defecto "src/Views/".
* @param string $extension (opcional) Extensión del archivo.
*
* @return void
*/
public function css(
string $viewName,
string $viewPath = null,
string $extension = 'css'
string $viewName,
?string $viewPath = null,
string $extension = 'css'
): void
{
header("Content-type: text/css");
@ -104,16 +104,16 @@ class View extends Neuron {
/**
* Renderiza código Javascript.
*
* @param string $viewName Ruta relativa y el nommbre sin extensión del archivo ubicado en src/Views
* @param string $viewPath (opcional) Ruta donde se encuentra la vista. En caso de que la vista no se encuentre en esa ruta, se usará la ruta por defecto "src/Views/".
* @param string $extension (opcional) Extensión del archivo.
* @param string $viewName Ruta relativa y el nommbre sin extensión del archivo ubicado en src/Views
* @param string|null $viewPath (opcional) Ruta donde se encuentra la vista. En caso de que la vista no se encuentre en esa ruta, se usará la ruta por defecto "src/Views/".
* @param string $extension (opcional) Extensión del archivo.
*
* @return void
*/
public function js(
string $viewName,
string $viewPath = null,
string $extension = 'js'
string $viewName,
?string $viewPath = null,
string $extension = 'js'
): void
{
header("Content-type: application/javascript");
@ -145,5 +145,20 @@ class View extends Neuron {
header('Content-Type: text/plain; charset=utf-8');
print($txt);
}
/**
* 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;
}
}
?>