Compare commits

157 Commits
Author SHA1 Message Date
kj 3b6da45b01 refactor(validator): rename enum rule and method to in 2026-09-14 16:48:02 -03:00
kj b7260b5199 refactor(Router): use string callable syntax for default callbacks 2026-09-08 12:08:32 -03:00
kj 1f29622914 refactor(router): remove exit statement from redirect method 2026-09-07 20:00:15 -03:00
kj eb882db0c8 sync: 2026-09-07
70b1016 refactor(autoload): remove global exception handler
a534613 refactor(request): propagate validation failures as exceptions
5f3fc16 feat(router): add error boundary with exception callback
9dbfd7d feat: add named parameter injection to resolve()
2026-09-07 18:00:18 -03:00
kj 9ba43e3f25 sync: 2026-09-05
25db49f fix(model): reset query state when database query fails
9cb41d5 fix(model): use RANDOM() for non-MySQL databases in orderBy
2026-09-05 16:49:10 -03:00
kj 54e96ba4bb sync: first sync with the new develop branch
a4611e8 chore(src): rename .keep files to .gitkeep
2026-09-05 14:21:52 -03:00
kj 9027f36748 refactor(model): use reflection for search field discovery 2026-09-05 00:39:16 -03:00
kj 2ae9ef39f3 feat(request): negotiate error response frmat based on Accept header 2026-09-03 18:23:42 -03:00
kj ee0db0307e docs(Validator): Expand documentation with complete rules reference 2026-09-03 18:22:52 -03:00
kj 44cbe6bbdc feat(validator): add file upload validation rules 2026-09-03 18:07:54 -03:00
kj 31728af351 docs: Clarify phpdoc 2026-09-03 16:12:54 -03:00
kj 4845d14ef5 chore(validator): suppress PHPCS rule for non-camel-case method 2026-09-03 15:59:48 -03:00
kj aca4e732ed feat(validation): Add human-readable validation error messages 2026-09-03 15:59:36 -03:00
kj f475c9125f feat(Validator): Add confirmed, required_with, and required_if rules 2026-09-03 15:03:28 -03:00
kj 4fe7314389 feat(Validator): Add regex and date validation methods 2026-09-03 14:42:15 -03:00
kj 1f366830bc feat(validation): add type and size-based validation methods
Add string, array, min, max, between, and size validators to the
Validator class. A private measure helper centralizes size semantics
for numbers (value), strings (length), and arrays (element count).
2026-09-03 14:35:02 -03:00
kj 15df7c8d96 refactor(Validator): improve rule argument parsing 2026-09-03 14:20:02 -03:00
kj b8dd1fd0f6 refactor(model): remove default 'id' from dbIgnoreSave list 2026-06-19 16:08:29 -03:00
kj 83c80bd4ab fix(model): prevent overwriting primary key on save 2026-06-19 15:34:47 -03:00
kj b13c07d3c4 fix(model): Remove the NULL check in the attribute loop 2026-06-19 15:29:51 -03:00
kj b95747bfe0 feat(model): add type casting for DateTime 2026-06-19 15:14:25 -03:00
kj 01c5eceeb4 refactor(model): Use match expression for PDO type detection 2026-06-19 14:52:25 -03:00
kj ee9c109ed9 fix(model): improve PDO parameter binding and update logic 2026-06-19 14:47:01 -03:00
kj 7eed69725d refactor(model): extract type casting logic into helper methods 2026-06-19 14:46:30 -03:00
kj e37035eed8 fix(model): support both backed and unit enums 2026-06-13 16:48:07 -03:00
kj 5312e5060e fix(model): reset dbQueryVariables before update and insert
Clear the static $dbQueryVariables array before performing update
and insert operations to prevent data leakage between queries.
2026-06-13 15:30:45 -03:00
kj 3c9db3ac53 refactor(neuron): remove return type hint from __get method 2026-06-05 13:28:19 -03:00
outcontrol 2b5d19ecd5 fix(synapsis): Skip variadic parameters in argument resolution 2026-04-15 16:35:32 -03:00
outcontrol 53e862d126 feat(exception): Register global exception handler
This handler catches uncaught exceptions and prints their message
and stack trace.
2026-03-16 06:38:26 -03:00
outcontrol b6baeef32f style(Synapsis): Wrap long exception message for readability 2026-03-16 06:25:09 -03:00
outcontrol 159da01d5c fix(synapsis): Include parameter name in untyped parameter error 2026-03-16 06:24:16 -03:00
outcontrol 3770dd6d9b refactor(model): Track model persistence state 2026-03-11 18:48:32 -03:00
outcontrol 76a69e5ef8 refactor(Model): Standarize property names 2026-03-11 18:22:16 -03:00
kj c5a4967ae4 refactor: Extract autoloader to dedicated file 2026-02-10 14:09:13 -03:00
kj c0b9f3851a chore(router): Remove redundant middleware count check 2026-02-02 10:23:48 -03:00
kj 78e199e300 feat(router): Introduce withMiddleware() for scoped middlewares 2026-02-01 08:25:54 -03:00
kj 8cd5e114dd refactor(router): Align type hint for optional parameter in docblock 2026-02-01 07:47:04 -03:00
kj ce03bdb27d fix(model): Fix postgresql search. 2025-10-27 15:16:54 -03:00
kj b24598b118 fix(sqlite): Correct base table for RIGHT JOIN conversion 2025-10-27 15:15:45 -03:00
kj 5019b89cc0 docs(readme): Update PostgreSQL compatibility status 2025-10-27 10:27:18 -03:00
kj d030e8d30e refactor(model)!: Allow groupBy to accept multiple args instead an array 2025-10-27 10:26:12 -03:00
kj b2672ad92f fix(model): Remove ON clause from cross join 2025-10-27 10:24:25 -03:00
kj 4c57770b43 refactor(model): Remove backticks from column names 2025-10-27 10:23:55 -03:00
kj 8e84450f95 fix(query): Adjust LIMIT OFFSET clause syntax 2025-10-27 10:23:31 -03:00
kj ece32b9bbe docs(readme): Update supported where clauses 2025-10-27 06:50:13 -03:00
kj a5cf9d239d refactor(model): Fix typo on WHERE EXISTS/NOT EXISTS methods 2025-10-27 06:46:17 -03:00
kj b9509c49ed refactor(model): Add string type hint to select columns 2025-10-21 16:26:39 -03:00
kj 6e433b4d06 feat(query-builder): Implement advanced WHERE and new JOIN clauses 2025-10-17 13:13:01 -03:00
kj 892b3614ec refactor(db): Detect DB type using PDO driver name 2025-10-17 13:12:02 -03:00
kj a0b544eae5 refactor(Model): Rename bindValue to bind and make public 2025-10-17 11:55:37 -03:00
kj 20fd78ab53 docs(Request): Remove param comment 2025-10-16 20:51:52 -03:00
kj f5411daaa5 Translate comments to English 2025-10-16 20:02:54 -03:00
kj 7e7ec68fd7 refactor(router): Use Neuron object for route parameters 2025-10-16 12:04:05 -03:00
kj ac9a661bc0 fix(htaccess): Block direct access to vendor and src folders 2025-10-11 12:15:15 -03:00
kj c8d7b69367 feat(bootstrap): Introduce Libs\Loader for directory loading 2025-10-11 10:44:20 -03:00
kj 7f62e06ff9 docs: Improve documentation and translate comments to English 2025-10-10 21:18:22 -03:00
kj 674c9d5ff4 fix(validator): Improve filter_var robustness in validation methods 2025-10-10 21:17:41 -03:00
kj 2f50532e13 refactor(Model): Type hint setNull variadic arguments 2025-10-10 21:06:11 -03:00
kj b41514a491 fix(di): Improve parameter resolution and validation 2025-10-10 19:15:07 -03:00
kj a1a15f492c refactor!: add Synapsis for dependency resolution and injection
- Remove Middleware class and custom callback handling logic.
- Implement Synapsis as a dependency injection container for automatic
resolution.
- Refactor Router to use Synapsis for process route callbacks and not found
handler.
- Update Request to remove middleware-specific properties and use
Router::$currentParams for path parameters.
2025-10-10 17:44:49 -03:00
kj b19e7d8789 Remove a forgotten debug line. 2025-09-07 18:52:33 -03:00
kj 4dfdb52519 Remove unnecessary and really never-used tableSufix property. 2025-09-07 15:25:48 -03:00
kj b0891885f9 BREAKING CHANGE: Make select, from, and setNull methods variadic. 2025-09-07 15:20:06 -03:00
kj b282d5479f Convert object/database naming conventions.
Adhere to PSR-12 by converting object properties to "lowerCamelCase" for objects
and "snake_case" for database interactions.

Backward compatibility is maintained: object properties already using
"snake_case" will continue to work without issue.
2025-09-07 15:01:45 -03:00
kj c9f467345b BREAKING CHANGE: Adhere to PSR-12 coding standards.
- Model: where_in method was renamed as whereIn.
2025-09-07 11:07:07 -03:00
kj 0f46848d15 Remove the predefined 'id' property.
This property is not necessary and gives less flexibility.
2025-08-15 14:51:16 -03:00
kj b2cb8d6883 Allow statically call beginTransaction, rollback and commit methods. 2025-07-15 18:12:41 -03:00
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
kj c65fcd27c8 Create new ROOT_CORE constant. 2024-05-09 18:36:48 -04:00
kj f5f803dde2 Reorganize code for PSR and others code prettify. 2024-05-09 15:13:52 -04:00
kj 6a1085b224 Allow Neuron as $params on view render. 2024-05-09 14:56:07 -04:00
kj 64bcfa0b00 Add Lib\Request. 2024-05-05 09:38:10 -04:00
kj cd01ab9e72 Change switch to match. 2023-10-20 16:26:17 -04:00
kj e2094ccb4a Add option to use another extensions, js and css methods. 2023-09-15 21:26:53 -04:00
kj 341d7837a1 Add Router::reconfigure method. 2023-09-08 22:42:50 -04:00
kj 030e1079dc add mising types, patch method and a new way to set middleware
- Removed resumed way to set controllers and middlewares:
  Now, the method of routers and middlewares need to be
  callables.
- Add patch method in order to support PATCH http method.
- Add a way to get add more middlewares after. For Example:

  <?php
    use Libs\Router;

    Router::get('/some-route', 'somefunction')
      ->middleware('firstmiddleware');
    Router::get('/other-route', 'otherfunction');

    // Recover first router and add a middleware
    Router::get('/some-route')->middleware('secondmiddleware');
  ?>
2023-08-22 01:48:04 -04:00
kj af673a68b8 Add exit after redirect in order to avoid other erros. 2023-08-09 08:54:17 -04:00
kj eff0b86762 Allow construct Neuron with named arguments.
Example:

<?php
$instance = new Neuron(stringValue: 'Hello world', integerValue: 50,
boolValue: false);

echo $instance->stringValue; // "Hello world" will be printed.
?>

Also, is possible to send infinite params without names and his names
will be numeric similar as a non-asociatie array, but as Neuron object.

Example:

<?php
$str = 'Hello world';
$int = 50;
$con = false;

$instance = new Neuron($str, $int);

echo $instance->{0}; // "Hello world" will be printed.
?>
2023-06-04 14:47:58 -04:00
kj 39a1f9d85a Improve magic function __get.
Is not necessary another conditionals. When __get is called
is only when the the property is not defined, so
only need return null in order to avoid the
PHP notice of undefined property.
2023-06-04 14:34:24 -04:00
kj d48f24ed98 Improve return types. 2023-04-22 05:32:37 -04:00
kj b326c8e1d0 Fix mising use. 2023-04-16 23:47:49 -04:00
kj 1267d6b2f6 Fix deprecation warnings on PHP8.2 2023-04-15 13:17:00 -04:00
kj a8d95cb55b Fix empty application/json. 2023-04-14 16:43:50 -04:00
kj 06b7907f7c Define mixed type to param $id on getById method. 2023-03-25 21:28:25 -04:00
kj 4d052efba6 Change return type from mixed to ?Model on getFirts and getById methods. 2023-03-25 18:28:21 -04:00
kj 11141a0eee Allow send string to setNull method. 2023-03-25 12:27:33 -04:00
kj 3c8a21161f Add missing docblocks and return types. 2023-03-25 12:26:42 -04:00
kj 1bde430251 Improve Neuron and allow use to clone another object. 2023-03-15 22:16:05 -04:00
kj 7d3d1615d5 Fix View:txt and add charset. 2022-11-22 00:59:20 -04:00
kj 404bd59569 Improve where, and, or and search method.
- static::$querySelect['AndOr'] is REMOVED.
- where, and, or and search now use only the
  static::$querySelect['where'] index.
- removed unnecesary brackets on buildQuery.
2022-08-22 15:26:32 -04:00
kj 53bdc92344 Add bindValue method to Libs\Model. 2022-08-18 16:38:40 -04:00
kj ad7b78f427 Fix prepare variables when special where/and/or columns. 2022-08-12 06:31:54 -04:00
kj 38d2a90318 Fix count when using joins. 2022-08-12 05:46:24 -04:00
kj 2411704662 Add sqlite support. 2022-08-04 05:30:22 -04:00
kj 08d92a2b81 Allow usage of bool types on the ORM. 2022-08-01 08:27:49 -04:00
kj 8d47e10d7a Fix resetQuery. 2022-07-29 22:23:07 -04:00
kj 2c7938a0c3 Set default fetch mode to fetch assoc. 2022-07-24 00:29:34 -04:00
kj c57ca191b1 Rename limit() variables with better names and add description. 2022-07-19 12:08:22 -04:00
kj f4eabe6d54 Minor fixes.
- Add some uses.
- Fix typo and order error con docl block.
2022-07-14 22:39:38 -04:00
kj 83d9ba55aa Update migration notes. 2022-07-14 22:08:43 -04:00
kj bad7bf0e65 Update config template. 2022-07-14 22:03:07 -04:00
kj 5284be2b1b Update org files. 2022-07-14 21:59:27 -04:00
kj 0db6e4a021 rework to change from MySQLi to PDO. 2022-07-14 21:41:48 -04:00
kj afb4c914a1 Fix error on where_in with string values. 2022-07-10 12:10:01 -04:00
kj 4c4fe6f1f7 Change View to work as instance and render text and json. 2022-06-25 21:11:11 -04:00
kj b6555ee039 Fix docblocks. 2022-06-07 23:06:37 -04:00
kj a641248453 Add middleware priorities. 2022-06-07 23:05:49 -04:00
kj eb27acf68e Add middleware lib. 2022-05-18 12:29:27 -04:00
kj f38c6610fb Allow multiple middlewares. 2022-05-18 12:29:07 -04:00
kj 4003a88f66 Insignificant change: Rename some variables. 2022-04-14 12:52:52 -04:00
kj 01927ec946 Normalize header comments to phpdoc compatibility. 2022-04-12 15:56:50 -04:00
kj a23e795f5c Fix error on getVars when get not set properties. 2022-04-05 17:12:30 -04:00
kj 9689ac82cf Fix wrong result type on getById. 2022-03-30 14:09:08 -04:00
kj fe61d14713 Fix error on save. 2022-03-28 21:55:29 -04:00
kj 97380b2bee Change ModelMySQL atributes to strong typed. 2022-03-22 04:30:47 -04:00
kj 4cd7850931 fix result type on ModelMySQL::query. 2022-03-17 22:38:10 -04:00
kj 4240b048fd add missing use for mysqli_result. 2022-03-17 22:31:47 -04:00
kj f43028d72d Migrate to strong typed (compatible only with PHP 7.0+). 2022-03-17 17:22:04 -04:00
21 changed files with 3640 additions and 1172 deletions
+3
View File
@@ -1,6 +1,9 @@
<IfModule mod_rewrite.c>
RewriteEngine On
# Bloquear acceso a las carpetas vendor/ y src/
RewriteRule ^(vendor|src)/.*$ - [F,L]
# Handle Front Controller...
RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_FILENAME} !-f
+14
View File
@@ -0,0 +1,14 @@
<?php
require_once('config.php');
// Autoloader for the core classes
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;
}
});
+10 -5
View File
@@ -1,10 +1,15 @@
<?php
define('dbhost', 'localhost');
define('dbname', '');
define('dbuser', '');
define('dbpass', '');
// Database configuration
define('DB_TYPE', 'mysql');
define('DB_HOST', 'localhost');
define('DB_NAME', '');
define('DB_USER', '');
define('DB_PASS', '');
// Site configuration
define('SITE_URL', '');
// Advanced configuration
define('ROOT_DIR', __DIR__);
?>
define('ROOT_CORE', ROOT_DIR . '/src');
+10 -19
View File
@@ -1,24 +1,15 @@
<?php
require_once('config.php');
// Incluir clases
spl_autoload_register(function ($className) {
$fp = str_replace('\\','/',$className);
$name = basename($fp);
$dir = dirname($fp);
$file = ROOT_DIR.'/src/'.$dir.'/'.$name.'.php';
if (file_exists($file)) {
require_once $file;
return;
}
});
use Libs\Loader;
use Libs\Router;
// Incluir routers
$routers = glob(ROOT_DIR.'/src/Routers/*.php');
require_once('autoload.php');
foreach($routers as $file){
require_once($file);
}
// Router autoload
Loader::load(ROOT_CORE . '/Routers/');
\Libs\Router::apply();
?>
// Other autoloads
Loader::load(ROOT_CORE . '/Autoloads/');
// Run the routers
Router::apply();
-21
View File
@@ -1,21 +0,0 @@
# DuckBrain - PHP Microframework
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.
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 este conjunto de librerías/microframework luego de ver como proyectos relativamente pequeños eran hechos con frameworks demasiado pesados para lo que eran.
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.
## Uso / Documentación
Queda pendiente, si quieres usarlo ya mismo, puedes leer los comeentarios que he colocado en el código.
## Contacto
Puedes encontrame en telegram como [@keyjay](https://telegram.me/keyjay) o contactarme mi correo: webmaster@outcontrol.net
+119
View File
@@ -0,0 +1,119 @@
#+TITLE: Duckbrain
#+AUTHOR: KJ
#+OPTIONS: toc:nil
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.
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.
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
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
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 BD, pero la librería lo traduce a un equivalente.
|------------------+---------------+---------+------------|
| method | MySQL/MariaDB | sqlite3 | postgreSQL |
|------------------+---------------+---------+------------|
| db | ok | ok | ok |
|------------------+---------------+---------+------------|
| query | ok | ok | ok |
|------------------+---------------+---------+------------|
| resetQuery | ok | ok | ok |
|------------------+---------------+---------+------------|
| buildQuery | ok | ok | ok |
|------------------+---------------+---------+------------|
| getInstance | ok | ok | ok |
|------------------+---------------+---------+------------|
| getVars | ok | ok | ok |
|------------------+---------------+---------+------------|
| className | ok | ok | pk |
|------------------+---------------+---------+------------|
| table | ok | ok | ok |
|------------------+---------------+---------+------------|
| update | ok | ok | ok |
|------------------+---------------+---------+------------|
| beginTransaction | ok | ok | ok |
|------------------+---------------+---------+------------|
| rollBack | ok | ok | ok |
|------------------+---------------+---------+------------|
| commit | ok | ok | ok |
|------------------+---------------+---------+------------|
| add | ok | ok | ok |
|------------------+---------------+---------+------------|
| save | ok | ok | ok |
|------------------+---------------+---------+------------|
| delete | ok | ok | ok |
|------------------+---------------+---------+------------|
| select | ok | ok | ok |
|------------------+---------------+---------+------------|
| from | ok | ok | ok |
|------------------+---------------+---------+------------|
| where | ok | ok | ok |
|------------------+---------------+---------+------------|
| whereIn | ok | ok | ok |
|------------------+---------------+---------+------------|
| whereNotIn | ok | ok | ok |
|------------------+---------------+---------+------------|
| whereNull | ok | ok | ok |
|------------------+---------------+---------+------------|
| whereNotNull | ok | ok | ok |
|------------------+---------------+---------+------------|
| whereExists | ok | ok | ok |
|------------------+---------------+---------+------------|
| whereNotExists | ok | ok | ok |
|------------------+---------------+---------+------------|
| leftJoin | ok | ok | ok |
|------------------+---------------+---------+------------|
| rightJoin | ok | fixed | ok |
|------------------+---------------+---------+------------|
| innerJoin | ok | ok | ok |
|------------------+---------------+---------+------------|
| crossJoin | ok | ok | ok |
|------------------+---------------+---------+------------|
| and | ok | ok | ok |
|------------------+---------------+---------+------------|
| or | ok | ok | ok |
|------------------+---------------+---------+------------|
| groupBy | ok | ok | ok |
|------------------+---------------+---------+------------|
| limit | ok | ok | ok |
|------------------+---------------+---------+------------|
| orderBy | ok | ok | ok |
|------------------+---------------+---------+------------|
| count | ok | ok | ok |
|------------------+---------------+---------+------------|
| getById | ok | ok | ok |
|------------------+---------------+---------+------------|
| search | ok | ok | ok |
|------------------+---------------+---------+------------|
| get | ok | ok | ok |
|------------------+---------------+---------+------------|
| getFirst | ok | ok | ok |
|------------------+---------------+---------+------------|
| all | ok | ok | ok |
|------------------+---------------+---------+------------|
| setNull | ok | ok | ok |
|------------------+---------------+---------+------------|
* Contacto
Puedes encontrame en telegram como [[https://telegram.me/keyjay][@keyjay]] o contactarme mi correo: webmaster@outcontrol.net
+51 -26
View File
@@ -1,38 +1,63 @@
<?php
/*
* DuckBrain - Microframework
*
* Clase diseñada para crear y devolver una única instancia mysqli (database).
* Depende de manera forzada de que estén definidas las constantes:
* dbhost, dbname, dbpass y dbuser
*
* Autor: KJ
* Web: https://kj2.me
* Licencia: MIT
*/
namespace Libs;
use mysqli;
class Database extends \mysqli {
static private $db;
use Exception;
use PDO;
use PDOException;
private function __construct() {}
/**
* Database - DuckBrain
*
* Class designed to create and return a single PDO instance (database).
*
* @author KJ
* @website https://kj2.me
* @license MIT
*/
class Database extends PDO
{
private static array $databases = [];
/**
* Devuelve una instancia homogénea (singlenton) a la base de datos.
*
* @return mysqli
* Private constructor to prevent direct instantiation.
*/
static public function getConnection() : mysqli {
if (!isset(self::$db)) {
self::$db = new mysqli(dbhost, dbuser, dbpass, dbname);
if (self::$db->connect_errno) {
echo '<style>body{white-space: pre-line;}</style>';
throw new \Exception('No se ha podido conectar a la base de datos.');
private function __construct()
{
}
/**
* Returns a homogeneous (singleton) instance of the database (PDO).
*
* @return PDO
* @throws Exception If there is an error connecting to the database.
*/
public static 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 self::$db;
return static::$databases[$key];
}
}
?>
+31
View File
@@ -0,0 +1,31 @@
<?php
namespace Libs;
/**
* Loader - DuckBrain
*
* Simple library to bulk load multiple php files inside a folder.
*
* @author KJ
* @website https://kj2.me
* @license MIT
*/
class Loader
{
/**
* Loads all PHP files from a specified directory.
* If the directory does not exist or is not a directory, no files will be loaded.
*
* @param string $directoryPath The path to the directory containing the PHP files to load.
* @return void
*/
public static function load(string $directoryPath): void
{
if (is_dir($directoryPath)) {
foreach (glob(rtrim($directoryPath, '/') . '/*.php') as $file) {
require_once($file);
}
}
}
}
+1201
View File
File diff suppressed because it is too large Load Diff
-826
View File
@@ -1,826 +0,0 @@
<?php
/**
* ModelMysql - DuckBrain
*
* Modelo ORM para objetos que hagan uso de una base de datos MySQL.
* Depende de que exista Libs\Database para poder funcionar.
*
* @autor KJ
* @website https://kj2.me
* @licence MIT
*/
namespace Libs;
use Libs\Database;
use mysqli;
class ModelMySQL {
public $id;
protected $toNull = [];
static protected $primaryKey = 'id';
static protected $ignoreSave = ['id'];
static protected $forceSave = [];
static protected $table;
static protected $tableSufix = 's';
static protected $db;
static protected $querySelect = [
'select' => ['*'],
'where' => '',
'from' => '',
'leftJoin' => '',
'rightJoin' => '',
'innerJoin' => '',
'AndOr' => '',
'orderBy' => '',
'groupBy' => '',
'limit' => '',
'sql_calc_found_rows' => false
];
/**
* Sirve para obtener la instancia de la base de datos.
*
* @return mysqli
*/
protected static function db() : mysqli {
if (is_null(static::$db))
static::$db = Database::getConnection();
return static::$db;
}
/**
* 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.
*
* @return mysqli_result
* Contiene el resultado de la llamada SQL.
*/
protected static function query($query) : mysqli_result {
$db = static::db();
$result = $db->query($query);
if ($db->errno) {
echo '<style>body{white-space: pre-line;}</style>';
throw new \Exception(
"\nFallo al consultar la base de datos\n" .
"Errno: $db->errno\n" .
"Error: $db->error\n" .
"Query: $query\n"
);
}
return $result;
}
/**
* Reinicia la configuración de la sentencia SQL.
*/
protected static function resetQuery() {
static::$querySelect = [
'select' => ['*'],
'where' => '',
'from' => '',
'leftJoin' => '',
'rightJoin' => '',
'innerJoin' => '',
'AndOr' => '',
'orderBy' => '',
'groupBy' => '',
'limit' => '',
'sql_calc_found_rows' => false
];
}
/**
* Construye la sentencia SQL a partir static::$querySelect y una vez
* construída, llama a resetQuery.
*
* @param bool $resetQuery
* Indica si el query debe reiniciarse o no (por defecto es true).
*
* @return string
* Contiene la sentencia SQL.
*/
protected static function buildQuery(bool $resetQuery = true) : string {
if (static::$querySelect['sql_calc_found_rows'])
$sql = 'SELECT SQL_CALC_FOUND_ROWS '.join(', ', static::$querySelect['select']);
else
$sql = 'SELECT '.join(', ', static::$querySelect['select']);
if (static::$querySelect['from'] != '') {
$sql .= ' FROM '.static::$querySelect['from'];
} else {
$sql .= ' FROM '.static::table();
}
if(static::$querySelect['innerJoin'] != '') {
$sql .= static::$querySelect['innerJoin'];
}
if (static::$querySelect['leftJoin'] != '') {
$sql .= static::$querySelect['leftJoin'];
}
if(static::$querySelect['rightJoin'] != '') {
$sql .= static::$querySelect['rightJoin'];
}
if (static::$querySelect['where'] != '') {
$sql .= ' WHERE '.static::$querySelect['where'];
if (static::$querySelect['AndOr'] != '') {
$sql .= static::$querySelect['AndOr'];
}
}
if (static::$querySelect['groupBy'] != '') {
$sql .= ' GROUP BY '.static::$querySelect['groupBy'];
}
if (static::$querySelect['orderBy'] != '') {
$sql .= ' ORDER BY '.static::$querySelect['orderBy'];
}
if (static::$querySelect['limit'] != '') {
$sql .= ' LIMIT '.static::$querySelect['limit'];
}
if ($resetQuery)
static::resetQuery();
return $sql;
}
/**
* Crea una instancia del objeto actual a partir de un arreglo.
*
* @param mixed $elem
* Puede recibir un arreglo o un objeto que contiene los valores
* que tendrán sus atributos.
*
* @return ModelMySQL
* Retorna un objeto de la clase actual.
*/
protected static function getInstance(array $elem = []) : ModelMySQL {
$class = get_called_class();
$instance = new $class;
foreach ($elem as $key => $value) {
$instance->$key = $value;
}
return $instance;
}
/**
* Devuelve los atributos a guardar de la case actual.
* Los atributos serán aquellos que seran public y
* no esten excluidos en static::$ignoresave y aquellos
* que sean private o protected pero estén en static::$forceSave.
*
* @return array
* Contiene los atributos indexados del objeto actual.
*/
protected function getVars() : array {
$reflection = new \ReflectionClass($this);
$properties = $reflection->getProperties(\ReflectionProperty::IS_PUBLIC);
$result = [];
foreach($properties as $property) {
$att = $property->name;
$result[$att] = $this->$att;
}
foreach (static::$ignoreSave as $del) {
unset($result[$del]);
}
foreach (static::$forceSave as $value) {
$result[$value] = $this->$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(substr(strrchr(get_called_class(), '\\'), 1));
}
/**
* Construye (a partir del nombre de la clase y el sufijo en static::$tableSufix)
* y/o develve el nombre de la tabla de la BD en la que se alojará o
* se aloja el objeto actual.
*
* @return string
*/
protected static function table() : string {
if (isset(static::$table))
return static::$table;
return static::className().static::$tableSufix;
}
/**
* Actualiza los valores en la BD con los valores del objeto actual.
*/
protected function update() {
$atts = $this->getVars();
foreach ($atts as $key => $value) {
if (isset($value)) {
$value = static::db()->real_escape_string($value);
if (in_array($key, $this->toNull))
$set[]="$key=NULL";
else
$set[]="$key='$value'";
} else {
if (in_array($key, $this->toNull))
$set[]="$key=NULL";
}
}
$table = static::table();
$pk = static::$primaryKey;
$pkv = $this->$pk;
$sql = "UPDATE $table SET ".join(', ', $set)." WHERE $pk='$pkv'";
static::query($sql);
}
/**
* Inserta una nueva fila en la base de datos a partir del
* objeto actual.
*/
protected function add() {
$db = static::db();
$atts = $this->getVars();
foreach ($atts as $key => $value) {
if (isset($value)) {
$into[] = "`$key`";
$values[] = "'".$db->real_escape_string($value)."'";
}
}
$table = static::table();
$sql = "INSERT INTO $table (".join(', ', $into).") VALUES (".join(', ', $values).")";
static::query($sql);
$pk = static::$primaryKey;
$this->$pk = $db->insert_id;
}
/**
* Revisa si el objeto a guardar es nuevo o no y según el resultado
* llama a update para actualizar o add para insertar una nueva fila.
*/
public function save() {
$pk = static::$primaryKey;
if (isset($this->$pk))
$this->update();
else
$this->add();
}
/**
* Elimina el objeto actual de la base de datos.
*/
public function delete() {
$atts = $this->getVars();
foreach ($atts as $key => $value) {
$value = static::db()->real_escape_string($value);
$set[]="$key='$value'";
}
$table = static::table();
$pk = static::$primaryKey;
$pkv = $this->$pk;
$sql = "DELETE FROM $table WHERE $pk='$pkv'";
static::query($sql);
}
/**
* Define SELECT en la sentencia SQL.
*
* @param array $columns
* Columnas que se selecionarán en la consulta SQL.
*
* @return ModelMySQL
*/
public static function select(array $columns) : ModelMySQL {
$db = static::db();
$select = [];
foreach($columns as $column) {
$select[] = $db->real_escape_string($column);
}
static::$querySelect['select'] = $select;
return new static();
}
/**
* Define FROM en la sentencia SQL.
*
* @param array $tables
* Tablas que se selecionarán en la consulta SQL.
*
* @return ModelMySQL
*/
public static function from(array $tables) : ModelMySQL {
$db = static::db();
$from = [];
foreach($tables as $table) {
$from[] = $db->real_escape_string($table);
}
static::$querySelect['from'] = join(', ', $from);
return new static();
}
/**
* Define el WHERE en la sentencia SQL.
*
* @param string $column
* La columna a comparar.
*
* @param string $operador
* 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_quote
* (opcional) Se usa cuando $value es una columna o un valor que no requiere comillas.
*
* @return ModelMySQL
*/
public static function where(string $column, string $operator, string $value=null, bool $no_quote = false) : ModelMySQL {
if (is_null($value)) {
$value = $operator;
$operator = '=';
}
$value = static::db()->real_escape_string($value);
if ($no_quote)
static::$querySelect['where'] = "$column$operator$value";
else
static::$querySelect['where'] = "$column$operator'$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 ModelMySQL
*/
public static function where_in(string $column, array $arr, bool $in = true) : ModelMySQL {
foreach($arr as $index => $value) {
$arr[$index] = static::db()->real_escape_string($value);
}
if ($in)
static::$querySelect['where'] = "$column IN (".join(', ',$arr).")";
else
static::$querySelect['where'] = "$column NOT IN (".join(', ',$arr).")";
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 $operador
* 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 ModelMySQL
*/
public static function leftJoin(string $table, string $columnA, string $operator, string $columnB = null) : ModelMySQL {
if (is_null($columnB)) {
$columnB = $operator;
$operator = '=';
}
$columnA = static::db()->real_escape_string($columnA);
$columnB = static::db()->real_escape_string($columnB);
static::$querySelect['leftJoin'] .= ' LEFT JOIN ' . $table . ' ON ' . "$columnA$operator$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 $operador
* 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 ModelMySQL
*/
public static function rightJoin(string $table, string $columnA, string $operator, string $columnB = null) : ModelMySQL {
if (is_null($columnB)) {
$columnB = $operator;
$operator = '=';
}
$columnA = static::db()->real_escape_string($columnA);
$columnB = static::db()->real_escape_string($columnB);
static::$querySelect['rightJoin'] .= ' RIGHT JOIN ' . $table . ' ON ' . "$columnA$operator$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 $operador
* 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 ModelMySQL
*/
public static function innerJoin(string $table, string $columnA, string $operator, string $columnB = null) : ModelMySQL {
if (is_null($columnB)) {
$columnB = $operator;
$operator = '=';
}
$columnA = static::db()->real_escape_string($columnA);
$columnB = static::db()->real_escape_string($columnB);
static::$querySelect['innerJoin'] .= ' INNER JOIN ' . $table . ' ON ' . "$columnA$operator$columnB";
return new static();
}
/**
* Define AND en la sentencia SQL (se puede anidar).
*
* @param string $column
* La columna a comparar.
*
* @param string $operador
* 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_quote
* (opcional) Se usa cuando $value es una columna o un valor que no requiere comillas.
*
* @return ModelMySQL
*/
public static function and(string $column, string $operator, string $value=null, bool $no_quote = false) : ModelMySQL {
if (is_null($value)) {
$value = $operator;
$operator = '=';
}
$value = static::db()->real_escape_string($value);
if ($no_quote)
static::$querySelect['AndOr'] .= " AND $column$operator$value";
else
static::$querySelect['AndOr'] .= " AND $column$operator'$value'";
return new static();
}
/**
* Define OR en la sentencia SQL (se puede anidar).
*
* @param string $column
* La columna a comparar.
*
* @param string $operador
* 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_quote
* (opcional) Se usa cuando $value es una columna o un valor que no requiere comillas.
*
* @return ModelMySQL
*/
public static function or(string $column, string $operator, string $value=null, bool $no_quote = false) : ModelMySQL {
if (is_null($value)) {
$value = $operator;
$operator = '=';
}
$value = static::db()->real_escape_string($value);
if ($no_quote)
static::$querySelect['AndOr'] .= " OR $column$operator$value";
else
static::$querySelect['AndOr'] .= " OR $column$operator'$value'";
return new static();
}
/**
* Define GROUP BY en la sentencia SQL.
*
* @param array $arr
* Columnas por las que se agrupará.
*
* @return ModelMySQL
*/
public static function groupBy(array $arr) : ModelMySQL {
static::$querySelect['groupBy'] = join(', ', $arr);
return new static();
}
/**
* Define LIMIT en la sentencia SQL.
*
* @param int $initial
* @param int $final
*
* @return ModelMySQL
*/
public static function limit(int $initial, int $final = 0) : ModelMySQL {
if ($final==0)
static::$querySelect['limit'] = $initial;
else
static::$querySelect['limit'] = $initial.', '.$final;
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 ModelMySQL
*/
public static function orderBy(string $value, string $order = 'ASC') : ModelMySQL {
if ($value == "RAND") {
static::$querySelect['orderBy'] = 'RAND()';
return new static();
}
$value = static::db()->real_escape_string($value);
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'],
'sql_calc_found_rows' => static::$querySelect['sql_calc_found_rows'],
'limit' => static::$querySelect['limit'],
'orderBy' => static::$querySelect['orderBy']
];
if ($useLimit && static::$querySelect['limit'] != '') {
static::$querySelect['select'] = ['1'];
static::$querySelect['sql_calc_found_rows'] = false;
static::$querySelect['orderBy'] = '';
$sql = 'SELECT COUNT(1) AS quantity FROM ('.static::buildQuery($resetQuery).') AS counted';
$queryResult = static::query($sql)->fetch_assoc();
$result = $queryResult['quantity'];
} else {
static::$querySelect['select'] = ['1'];
static::$querySelect['sql_calc_found_rows'] = true;
static::$querySelect['limit'] = '1';
static::$querySelect['orderBy'] = '';
$sql = static::buildQuery($resetQuery);
static::query($sql);
$result = static::found_row();
}
if (!$resetQuery) {
static::$querySelect['select'] = $backup['select'];
static::$querySelect['sql_calc_found_rows'] = $backup['sql_calc_found_rows'];
static::$querySelect['limit'] = $backup['limit'];
static::$querySelect['orderBy'] = $backup['orderBy'];
}
return $result;
}
/**
* Retorna las filas contadas en el último query.
*
* @return int
*/
public static function found_row() : int {
$result = static::query('SELECT FOUND_ROWS() AS quantity')->fetch_assoc();
return $result['quantity'];
}
/**
* Habilita el conteo de todos las coincidencias posibles incluso usando limit.
*
* @return ModelMySQL
*/
public static function sql_calc_found_rows() : ModelMySQL {
static::$querySelect['sql_calc_found_rows'] = true;
return new static();
}
/**
* Obtiene una instancia según su primary key (generalmente id).
*
* @param mixed $id
* @return ModelMySQL
*/
public static function getById($id) : ModelMySQL {
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 ModelMySQL
*/
public static function search(string $search, array $in = null) : ModelMySQL {
if ($in == null) {
$className = get_called_class();
$in = array_keys((new $className())->getVars());
}
$db = static::db();
$search = $db->real_escape_string($search);
$where = [];
foreach($in as $row) {
$where[] = "$row LIKE '%$search%'";
}
if (static::$querySelect['where']=='')
static::$querySelect['where'] = join(' OR ', $where);
else
static::$querySelect['where'] = static::$querySelect['where'] .' AND ('.join(' OR ', $where).')';
return new static();
}
/**
* Obtener los resultados de la consulta SQL.
*
* @param bool $resetQuery
* (opcional) Indica si el query debe reiniciarse o no (por defecto es true).
*
* @return array
* Contiene un arreglo de instancias de la clase actual.
*/
public static function get(bool $resetQuery = true) : array { // Devuelve array vacío si no encuentra nada.
$sql = static::buildQuery($resetQuery);
$result = static::query($sql);
$instances = [];
while ($row = $result->fetch_assoc()) {
$instances[] = static::getInstance($row);
}
return $instances;
}
/**
* El primer elemento de la consulta SQL.
*
* @param bool $resetQuery
* (opcional) Indica si el query debe reiniciarse o no (por defecto es true).
*
* @return mixed
* Puede retornar un objeto ModelMySQL o null.
*/
public static function getFirst(bool $resetQuery = true) { // Devuelve null si no encuentra nada.
static::limit(1);
$instances = static::get($resetQuery);
return empty($instances) ? null : $instances[0];
}
/**
* Obtener todos los elementos del la tabla de la instancia actual.
*
* @return array
* Contiene un arreglo de instancias de la clase actual.
*/
public static function all() : array {
$sql = 'SELECT * FROM '.static::table();
$result = static::query($sql);
$instances = [];
while ($row = $result->fetch_assoc()) {
$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.
*
* @trows \Exception
* Devolverá un error en caso de usarse en un insert.
*
* @param array $atts
*/
public function setNull(array $atts) {
if (!isset($this->id))
throw new \Exception(
"\nEl método setNull sólo funciona para actualizar, no al insertar."
);
foreach ($atts as $att) {
if (!in_array($att, $this->toNull))
$this->toNull[] = $att;
}
}
}
?>
+49 -29
View File
@@ -1,39 +1,59 @@
<?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.
*
* @autor KJ
* @website https://kj2.me
* @licence MIT
*/
namespace Libs;
class Neuron {
use AllowDynamicProperties;
private $data;
/**
* Neuron - DuckBrain
*
* Neuron, serves to create an object that will hold values.
* In addition, it has the special characteristic that when trying
* to access an undefined property, it will return null instead
* of generating a notice (PHP notice) for an undefined variable or property.
*
* The constructor accepts an object or an array containing the
* values that will be defined.
*
* @author KJ
* @website https://kj2.me
* @license MIT
*/
#[AllowDynamicProperties]
class Neuron
{
/**
* Constructor
*
* @param array $data Data to initialize the Neuron with. Can be an array or an object.
*/
public function __construct(...$data)
{
if (
count($data) === 1 &&
isset($data[0]) &&
(is_array($data[0]) ||
is_object($data[0]))
) {
$data = $data[0];
}
public function __construct($data = []){
$this->data = (array) $data;
foreach ($data as $key => $value) {
$this->{$key} = $value;
}
}
public function __isset($index) {
return isset($this->data[$index]);
}
public function __get($index){
return (isset($this->data[$index]) && $this->data[$index] != '')
? $this->data[$index] : null;
/**
* Magic method __get
*
* This method is called when an undefined property is accessed.
* It returns null instead of triggering an E_NOTICE.
*
* @param string $index The name of the property being accessed.
* @return null Always returns null for undefined properties.
*/
public function __get(string $index)
{
return null;
}
}
?>
+180
View File
@@ -0,0 +1,180 @@
<?php
namespace Libs;
use Exception;
/**
* Request - DuckBrain
*
* Complementary library to the Router library.
* Contains the basic body of the http request (POST, GET, JSON, etc).
*
* @author KJ
* @website https://kj2.me
* @license MIT
*/
class Request extends Neuron
{
public Neuron $get;
public Neuron $post;
public Neuron $put;
public Neuron $patch;
public Neuron $delete;
public Neuron $json;
public Neuron $params;
public string $path;
public string $error;
public string $body;
/**
* __construct
*/
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();
$this->delete = new Neuron();
$this->params = Router::$params ?? 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($this->body), false)
);
} else {
$this->json = new Neuron();
if (
in_array($_SERVER['REQUEST_METHOD'], ['PUT', 'PATCH', 'DELETE']) &&
preg_match('/^[^;?\/:@&=+$,]{1,255}[=]/', $this->body, $matches)
) {
// With the regular expression, we verify that it is a valid
// http query string and avoid memory errors in case
// the body contains something larger than that.
parse_str($this->body, $input_vars);
$this->{strtolower($_SERVER['REQUEST_METHOD'])} = new Neuron($input_vars);
}
}
// Run configured validations
$this->validate();
}
/**
* Starts the configured validation.
*
* On failure the single error message is built with Validator::message()
* and handed to onInvalid(), which throws by default: failures travel up
* as exceptions instead of answering HTTP here.
*
* @return void
* @throws Exception When a configured rule fails.
*/
public function validate(): void
{
$actual = match ($_SERVER['REQUEST_METHOD']) {
'POST', 'PUT', 'PATCH', 'DELETE' => $this->{strtolower($_SERVER['REQUEST_METHOD'])},
default => $this->get
};
// Merge uploaded files ($_FILES) into the body data set so the file rules
// (file/image/mimes and the routed min/max/required...) can validate
// uploads through a Request. $this->post is left untouched.
$body = empty($_FILES)
? $actual
: new Neuron(array_merge(get_object_vars($actual), $_FILES));
if (
Validator::validateList(static::paramRules(), $this->params) &&
Validator::validateList(static::getRules(), $this->get) &&
Validator::validateList(static::rules(), $body)
) {
return;
}
$error = Validator::message(
Validator::$lastFailed,
static::messages(),
static::attributes()
);
static::onInvalid($error);
}
/**
* Rules for the current method.
*
* @return array
*/
public function rules(): array
{
return [];
}
/**
* Rules for URL parameters.
*
* @return array
*/
public function paramRules(): array
{
return [];
}
/**
* Rules for GET parameters.
*
* @return array
*/
public function getRules(): array
{
return [];
}
/**
* Error messages in case a validation fails.
*
* @return array
*/
public function messages(): array
{
return [];
}
/**
* Human-readable names for the fields.
*
* Forwarded to Validator::message() so the :attribute marker in the error
* text can be replaced with a friendlier name (e.g. "edad" => "la edad").
*
* @return array
*/
public function attributes(): array
{
return [];
}
/**
* Function to execute when an invalid value has been detected.
*
* The default implementation always throws a generic \Exception carrying
* the single error message and HTTP 422 as its code; the framework's
* exception boundary (Router::apply) renders the response. Override it to
* throw a more specific exception type instead. The never return type is
* the contract: an override that returned would let the request continue
* with invalid data, so PHP rejects such an override at compile time.
*
* @param string $error
*
* @return never
* @throws Exception
*/
public function onInvalid(string $error): never
{
throw new Exception($error, 422);
}
}
+387 -221
View File
@@ -1,48 +1,141 @@
<?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.
*
* Autor: KJ
* Web: https://kj2.me
* Licencia: MIT
*/
namespace Libs;
class Router {
private static $get = [];
private static $post = [];
private static $put = [];
private static $delete = [];
private static $last;
public static $notFoundCallback = 'Libs\Router::defaultNotFound';
/**
* Router - DuckBrain
*
* Router library.
* It strictly depends on the ROOT_DIR constant being defined
* and optionally on the SITE_URL constant being defined.
*
* @author KJ
* @website https://kj2.me
* @license MIT
*/
class Router
{
private static array $get = [];
private static array $post = [];
private static array $put = [];
private static array $patch = [];
private static array $delete = [];
private static function defaultNotFound () {
/**
* Stores a temporary stack of middlewares to be applied to routes defined
* within a specific scope, typically configured by `withMiddleware()`.
* These middlewares are active only during the execution of the scope's callback.
*
* @var array $withMiddlewares<callable>
*/
private static array $withMiddlewares = [];
/**
* Stores the method and index of the last configured route, e.g., ['get', 0].
* Used for chaining methods like middleware() or reconfigure().
*
* @var array $last
*/
private static array $last;
/**
* Stores the parameters extracted from the current matching route.
*
* @var Neuron $params
*/
public static Neuron $params;
/**
* The callback function to be executed when no route matches.
*
* @var callable $notFoundCallback
*/
public static $notFoundCallback = 'Libs\Router::defaultNotFound';
/**
* Default callback function for when
* the route is not found.
*
* @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>';
echo '<h2 style="text-align: center;margin: 25px 0px;">Error 404 - Page Not Found</h2>';
}
private function __construct() {}
/**
* The callback function to be executed when the router's boundary
* catches an exception thrown anywhere in the matched route chain.
* It receives the exception as a named argument: the handler must
* declare its parameter as $exception (typeable as \Throwable).
*
* @var callable $exceptionCallback
*/
public static $exceptionCallback = 'Libs\Router::defaultException';
/*
* Parsea para deectar las pseudovariables (ej: {variable})
/**
* Default callback function for exception responses.
*
* The HTTP status comes from the exception's code only when it is an
* integer in the 400-599 range; anything else (0, arbitrary codes, the
* SQLSTATE string carried by PDOException) answers 500. The body is
* negotiated from the request's Accept header: JSON when the client asks
* for application/json, plain text otherwise. The trace is serialized
* from getTraceAsString() because json_encode() of a Throwable yields an
* empty object: its properties are protected.
*
* @param \Throwable $exception
*
* @return void
*/
public static function defaultException(\Throwable $exception): void
{
$code = $exception->getCode();
http_response_code(is_int($code) && $code >= 400 && $code <= 599 ? $code : 500);
$accept = $_SERVER['HTTP_ACCEPT'] ?? '';
if (str_contains($accept, 'application/json')) {
header('Content-Type: application/json');
print(json_encode([
'error' => $exception->getMessage(),
'exception' => get_class($exception),
'file' => $exception->getFile(),
'line' => $exception->getLine(),
'trace' => explode(PHP_EOL, $exception->getTraceAsString()),
]));
return;
}
print($exception->getMessage() . PHP_EOL . $exception->getTraceAsString());
}
/**
* __construct
*/
private function __construct()
{
}
/**
* Parses to detect pseudovariables (e.g., {variable})
*
* @param string $path
* Ruta con pseudovariables.
* Route with pseudovariables.
*
* @param mixed $callback
* Callback que será llamado cuando la ruta configurada en $path coincida.
* @param callable $callback
* Callback that will be called when the route configured in $path matches.
*
* @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.
* Array with 3 indices:
* path - Contains the route with pseudovariables replaced by regular expressions.
* callback - Contains the callback in Namespace\Class::Method format.
* paramNames - An array of parameter names found in the path.
*/
private static function parse(string $path, $callback) : array {
private static function parse(string $path, callable $callback): array
{
preg_match_all('/{(\w+)}/s', $path, $matches, PREG_PATTERN_ORDER);
$paramNames = $matches[1];
@@ -50,254 +143,327 @@ class Router {
$path = preg_replace(
['/\\\{\w+\\\}/s'],
['([^\/]+)'],
$path);
if (!is_callable($callback)) {
$callback = 'Controllers\\'.$callback;
}
$path
);
return [
'path' => $path,
'callback' => $callback,
'paramNames' => $paramNames
'callback' => [$callback],
'paramNames' => $paramNames,
];
}
/*
* Devuelve el ruta base o raiz del proyecto sobre la que trabajará el router.
/**
* Returns the base or root path of the project on which the router will work.
*
* Ej: Si la url del sistema está en "https://ejemplo.com/duckbrain"
* entonces la ruta base sería "/duckbrain"
* Ex: If the system URL is "https://example.com/duckbrain"
* then the base path would be "/duckbrain"
*
* @return string
*/
public static function basePath() : string {
if (defined('SITE_URL'))
return parse_url(SITE_URL, PHP_URL_PATH);
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.
/**
* Redirects to an internal relative path.
*
* @param string $path
* La ruta relativa a la ruta base.
* The path relative to the base path.
*
* Ej: Si nuesto sistema está en "https://ejemplo.com/duckbrain"
* llamamos a Router::redirect('/docs'), entonces seremos
* redirigidos a "https://ejemplo.com/duckbrain/docs".
* Ex: If our system is at "https://example.com/duckbrain"
* and we call Router::redirect('/docs'), we will be
* redirected to "https://example.com/duckbrain/docs".
* @return void
*/
public static function redirect(string $path) {
header('Location: '.static::basePath().substr($path,1));
public static function redirect(string $path): void
{
header('Location: ' . static::basePath() . ltrim($path, '/'));
}
/*
* Añade un middleware a la última ruta usada.
* Solo se puede usar un middleware a la vez.
/**
* Adds a middleware to the last used route.
* Only one middleware can be added at a time.
*
* @param mixed $callback
* @param callable $callback
* @param int|null $priority Optional priority for the middleware execution order.
*
* @return Router
* Devuelve un enlace estático.
* @return static
* Returns the current instance.
*/
public static function middleware($callback) : Router{
if (!isset(static::$last))
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 (!is_callable($callback)) {
$callback = 'Middlewares\\'.$callback;
if (isset($priority) && $priority <= 0) {
$priority = 1;
}
static::$$method[$index]['middleware'] = $callback;
return new static();
}
/*
* @return Neuron
* Devuelve un objeto que contiene los atributos:
* post - Donde se encuentran los valores enviados por $_POST.
* get - Donde se encuentran los valores enviados por $_GET.
* json - Donde se encuentran los valores JSON enviados en el body.
*
*/
private static function getReq() : Neuron {
$req = new Neuron();
$req->get = new Neuron($_GET);
$req->post = new Neuron($_POST);
$req->json = new Neuron(static::get_json());
$req->params = new Neuron();
$req->path = static::currentPath();
return $req;
}
/*
* @return object
* Devuelve un objeto con los datos recibidos en JSON.
*/
private static function get_json() : object {
$contentType = isset($_SERVER["CONTENT_TYPE"]) ? trim($_SERVER["CONTENT_TYPE"]) : '';
if ($contentType === "application/json") {
return json_decode(trim(file_get_contents("php://input")));
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 (object) '';
}
/*
* Define los routers para el método GET.
*
* @param string $path
* Ruta con pseudovariables.
*
* @param mixed $callback
* Callback que será llamado cuando la ruta configurada en $path coincida.
*
* @return Router
* Devuelve un enlace estático.
*/
public static function get(string $path, $callback) {
static::$get[] = static::parse($path, $callback);
static::$last = ['get', count(static::$get)-1];
return new static();
}
/*
* Define los routers para el método POST.
*
* @param string $path
* Ruta con pseudovariables.
*
* @param mixed $callback
* Callback que será llamado cuando la ruta configurada en $path coincida.
*
* @return Router
* Devuelve un enlace estático.
*/
public static function post(string $path, $callback) : Router {
static::$post[] = static::parse($path, $callback);
static::$last = ['post', count(static::$post)-1];
return new static();
}
/*
* Define los routers para el método PUT.
*
* @param string $path
* Ruta con pseudovariables.
*
* @param mixed $callback
* Callback que será llamado cuando la ruta configurada en $path coincida.
*
* @return Router
* Devuelve un enlace estático
*/
public static function put(string $path, $callback) : Router {
static::$put[] = static::parse($path, $callback);
static::$last = ['put', count(static::$put)-1];
return new static();
}
/*
* Define los routers para el método DELETE.
*
* @param string $path
* Ruta con pseudovariables
/**
* Temporarily applies a set of middlewares to routes defined within a given callback.
* The middlewares are only active for the duration of the callback execution
* and are reset afterwards.
*
* @param callable $middleware
* @param callable $callback
* Callback que será llamado cuando la ruta configurada en $path coincida.
*
* @return static
* Devuelve un enlace estático
*/
public static function delete(string $path, $callback) : Router {
static::$delete[] = static::parse($path, $callback);
static::$last = ['delete', count(static::$delete)-1];
public static function withMiddleware(callable $middleware, callable $callback): static
{
$currentMiddlewares = static::$withMiddlewares;
array_unshift(static::$withMiddlewares, $middleware);
$callback();
static::$withMiddlewares = $currentMiddlewares; // Restore withMiddleware
return new static();
}
/*
* Devuelve la ruta actual.
/**
* Reconfigures the final callback of the last route.
*
* @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();
}
/**
* Configures any method for all routes.
*
* If no callback is received, it searches for the current route
* and only sets the route as the last configured one
* provided it has been configured previously.
*
* @param string $method
* HTTP method.
* @param string $path
* Route with pseudovariables.
* @param callable|null $callback
*
* @return static
* Returns the current instance.
*/
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];
foreach (static::$withMiddlewares as $middleware) {
static::middleware($middleware);
}
return new static();
}
/**
* Defines routers for the GET method.
*
* @param string $path
* Route with pseudovariables.
* @param callable|null $callback
* Callback that will be called when the route configured in $path matches.
*
* @return static
* Returns the current instance.
*/
public static function get(string $path, ?callable $callback = null): static
{
return static::configure('get', $path, $callback);
}
/**
* Defines routers for the POST method.
*
* @param string $path
* Route with pseudovariables.
* @param callable|null $callback
* Callback that will be called when the route configured in $path matches.
*
* @return static
* Returns the current instance.
*/
public static function post(string $path, ?callable $callback = null): static
{
return static::configure('post', $path, $callback);
}
/**
* Defines routers for the PUT method.
*
* @param string $path
* Route with pseudovariables.
* @param callable|null $callback
* Callback that will be called when the route configured in $path matches.
*
* @return static
* Returns the current instance
*/
public static function put(string $path, ?callable $callback = null): static
{
return static::configure('put', $path, $callback);
}
/**
* Defines routers for the PATCH method.
*
* @param string $path
* Route with pseudovariables.
* @param callable|null $callback
* Callback that will be called when the route configured in $path matches.
*
* @return static
* Returns the current instance
*/
public static function patch(string $path, ?callable $callback = null): static
{
return static::configure('patch', $path, $callback);
}
/**
* Defines routers for the DELETE method.
*
* @param string $path
* Route with pseudovariables
* @param callable|null $callback
* Callback that will be called when the route configured in $path matches.
*
* @return static
* Returns the current instance
*/
public static function delete(string $path, ?callable $callback = null): static
{
return static::configure('delete', $path, $callback);
}
/**
* Returns the current path, taking the DuckBrain installation path as the root.
*
* @return string
*/
public static function currentPath() : string {
return preg_replace('/'.preg_quote(static::basePath(), '/').'/',
'/', strtok($_SERVER['REQUEST_URI'], '?'), 1);
public static function currentPath(): string
{
return preg_replace(
'/' . preg_quote(static::basePath(), '/') . '/',
'/',
strtok($_SERVER['REQUEST_URI'], '?'),
1
);
}
/*
* Aplica los routers.
/**
* Applies the route configuration.
*
* Este método ha de ser llamado luego de que todos los routers hayan sido configurados.
* This method is the framework's error boundary: any \Throwable thrown
* while running the matched route's callback chain, while printing the
* returned data, or while resolving the not-found callback is caught
* here and rendered through $exceptionCallback. A failing handler is
* deliberately left uncaught (double failure falls back to PHP itself).
*
* 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.
* @param string|null $path (optional) Path to use. If not defined, it detects the current path.
*
* 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() {
$path = static::currentPath();
$routers = [];
switch ($_SERVER['REQUEST_METHOD']){ // Según el método selecciona un arreglo de routers configurados
case 'POST':
$routers = static::$post;
break;
case 'PUT':
$routers = static::$put;
break;
case 'DELETE':
$routers = static::$delete;
break;
default:
$routers = static::$get;
break;
}
public static function apply(?string $path = null): void
{
try {
$path = $path ?? static::currentPath();
$routers = match ($_SERVER['REQUEST_METHOD']) { // Selects an array of routers based on the method
'POST' => static::$post,
'PUT' => static::$put,
'PATCH' => static::$patch,
'DELETE' => static::$delete,
default => static::$get
};
$args = static::getReq();
foreach ($routers as $router) { // Checks all routers to see if they match the current path
if (preg_match_all('/^' . $router['path'] . '\/?$/si', $path, $matches, PREG_PATTERN_ORDER)) {
unset($matches[0]);
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
if (isset($matches[1])) {
foreach ($matches as $index => $match) {
$paramName = $router['paramNames'][$index-1];
$args->params->$paramName = urldecode($match[0]);
// Checking and storing the variable parameters of the route
if (isset($matches[1])) {
static::$params = new Neuron();
foreach ($matches as $index => $match) {
$paramName = $router['paramNames'][$index - 1];
static::$params->{$paramName} = urldecode($match[0]);
}
}
// Processes the callback queue
foreach (array_reverse($router['callback']) as $callback) {
$data = Synapsis::resolve($callback);
}
// By default, prints as JSON if something is returned
if (isset($data)) {
header('Content-Type: application/json');
print(json_encode($data));
}
return;
}
// Comprobando si hay middleware
if (isset($router['middleware']))
$data = call_user_func_array($router['middleware'], [$router['callback'], $args]);
else
$data = call_user_func_array($router['callback'], [$args]);
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, [$args]);
// If no router matches, call $notFoundCallBack
Synapsis::resolve(static::$notFoundCallback);
} catch (\Throwable $exception) {
Synapsis::resolve(static::$exceptionCallback, ['exception' => $exception]);
}
}
}
?>
+179
View File
@@ -0,0 +1,179 @@
<?php
namespace Libs;
use Closure;
use Exception;
use ReflectionClass;
use ReflectionFunction;
use ReflectionMethod;
use ReflectionParameter;
/**
* Synapsis - DuckBrain
*
* Class responsible for dependency resolution and injection.
*
* @author KJ
* @website https://kj2.me
* @license MIT
*/
class Synapsis
{
/**
* @var array<class-string, class-string> $bindings
*/
private static array $bindings = [];
/**
* @var array<class-string, mixed> $instances
*/
private static array $instances = [];
/**
* Binds an interface to a class to instantiate it instead.
*
* @param class-string $interfaceName
* @param class-string $className
*
* @return void
* @throws Exception If the interface or class does not exist.
*/
public static function bind(string $interfaceName, string $className): void
{
if (interface_exists($interfaceName) && class_exists($className)) {
static::$bindings[$interfaceName] = $className;
return;
}
throw new Exception('Error at binding non existant Interface or ClassName.');
}
/**
* Binds multiple interfaces to multiple classes in bulk.
*
* @param array<class-string, class-string> $bindings
*
* @return void
*/
public static function bulkBind(array $bindings): void
{
foreach ($bindings as $interfaceName => $className) {
static::bind($interfaceName, $className);
}
}
/**
* Resolves and injects dependencies for a callable and returns its result.
*
* @param callable $action
* @param array<string, mixed> $named Associative array of values injected into the callable's
* parameters by exact name match. Consumed in resolveParameterValues().
*
* @return mixed
* @throws Exception If an unhandled callable type is provided.
*/
public static function resolve(callable $action, array $named = []): mixed
{
if ($action instanceof Closure) { // If it's an anonymous function
$reflectionCallback = new ReflectionFunction($action);
} else { // Array or string with the method
if (is_string($action)) {
$action = preg_split('/::/', $action);
}
// Check if it's a method or just a function
if (count($action) == 2) {
$reflectionCallback = new ReflectionMethod($action[0], $action[1]);
} else {
$reflectionCallback = new ReflectionFunction($action[0]);
}
}
// Get the parameters
return call_user_func_array(
$action,
static::resolveParameterValues($reflectionCallback->getParameters(), $named)
);
}
/**
* Resolves and injects dependencies for a class and returns its result.
*
* @param class-string $className
*
* @return mixed
* @throws Exception If a binding is missing for an interface or the class does not exist.
*/
public static function &resolveInstance(string $className): mixed
{
if (interface_exists($className)) {
if (isset(static::$bindings[$className])) {
$className = static::$bindings[$className];
} else {
throw new Exception("Missing binding for interface: {$className}");
}
}
if (!class_exists($className)) {
throw new Exception("Cannot resolve: {$className}");
}
if (!isset(static::$instances[$className])) {
// If the instance has not been resolved before, resolve it
$reflectionClass = new ReflectionClass($className);
$constructor = $reflectionClass->getConstructor();
static::$instances[$className] = new $className(
...static::resolveParameterValues($constructor?->getParameters() ?? [])
);
}
return static::$instances[$className];
}
/**
* Resolves parameter values by injecting dependencies.
*
* @param array<ReflectionParameter> $parameters
* @param array<string, mixed> $named
* Values injected into parameters whose name matches the key,
* taking precedence over optional defaults and DI resolution.
* Keys matching no parameter are ignored.
*
* @return array<mixed>
* @throws Exception If a primitive parameter does not have a default value.
*/
public static function resolveParameterValues(array $parameters, array $named = []): array
{
$values = [];
foreach ($parameters as $parameter) {
if ($parameter->isVariadic()) { // Skip variatic parameters
continue;
}
if (array_key_exists($parameter->getName(), $named)) { // Named values win over defaults and DI
$values[] = $named[$parameter->getName()];
continue;
}
if ($parameter->isOptional()) { // Always use the default value first
$values[] = $parameter->getDefaultValue();
continue;
}
$paramType = $parameter->getType();
if ($paramType === null) {
// If no type is declared, and it's not optional, we cannot resolve it.
throw new Exception(
"Untyped parameter '{$parameter}' expect at least a default value or must be optional."
);
}
if ($paramType->isBuiltin()) {
throw new Exception("Primitive parameter '{$parameter->getName()}' expects at least a default value.");
}
$values[] = static::resolveInstance($paramType->getName());
}
return $values;
}
}
File diff suppressed because it is too large Load Diff
+151 -25
View File
@@ -1,38 +1,164 @@
<?php
/**
* View - DuckBrain
*
* Manejador de vistas simplificado.
*
* @autor KJ
* @website https://kj2.me
* @licence MIT
*/
namespace Libs;
class View {
/**
* View - DuckBrain
*
* Simplified view handler.
*
* @author KJ
* @website https://kj2.me
* @license MIT
*/
class View extends Neuron
{
/**
* Includes the file.
*
* @param string $viewName Relative path and filename without extension.
* @param string|null $viewPath (Optional) Path where the view file is located.
* @param string $extension (Optional) File extension.
*
* @return void
*/
protected function include(
string $viewName,
?string $viewPath = null,
string $extension = 'php'
): void {
if (
isset($viewPath) &&
file_exists("$viewPath$viewName.$extension")
) {
include("$viewPath$viewName.$extension");
return;
}
include(ROOT_CORE . "/Views/$viewName.$extension");
}
/**
* Función que "renderiza" las vistas
* Function that "renders" views.
*
* @param string $viewName
* Ruta relativa y el nommbre sin extensión del archivo ubicado en src/Views
* @param string $viewName Relative path and filename without extension.
* @param array|Neuron $params (Optional) Array that can be used in the view
* via $view ($param['index'] would be used as: $view->index).
* @param string|null $viewPath (Optional) Path where the view is located. If the
* view is not found in this path, the default path "src/Views/" will be used.
* @param string $extension (Optional) File extension.
*
* @param array $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/".
* @return void
*/
public static function render(string $viewName, array $params = [], string $viewPath = null) {
$view = new Neuron($params);
unset($params);
public static function render(
string $viewName,
array|Neuron $params = [],
?string $viewPath = null,
string $extension = 'php'
): void {
$instance = new View($params);
$instance->html($viewName, $viewPath, $extension);
}
if (isset($viewPath) && file_exists($viewPath.$viewName.'.php'))
return include($viewPath.$viewName.'.php');
/**
* Renders HTML views.
*
* @param string $viewName Relative path and filename without extension located in src/Views.
* @param string|null $viewPath (Optional) Path where the view is located. If the view is not
* found in this path, the default path "src/Views/" will be used.
* @param string $extension (Optional) File extension.
*
* @return void
*/
public function html(
string $viewName,
?string $viewPath = null,
string $extension = 'php'
): void {
$this->include(
$viewName,
$viewPath,
$extension
);
}
include(ROOT_DIR.'/src/Views/'.$viewName.'.php');
/**
* Renders CSS code.
*
* @param string $viewName Relative path and filename without extension located in src/Views.
* @param string|null $viewPath (Optional) Path where the view is located. If the view is not
* found in this path, the default path "src/Views/" will be used.
* @param string $extension (Optional) File extension.
*
* @return void
*/
public function css(
string $viewName,
?string $viewPath = null,
string $extension = 'css'
): void {
header("Content-type: text/css");
$this->include($viewName, $viewPath, $extension);
}
/**
* Renders Javascript code.
*
* @param string $viewName Relative path and filename without extension located in src/Views.
* @param string|null $viewPath (Optional) Path where the view is located. If the view is not
* found in this path, the default path "src/Views/" will be used.
* @param string $extension (Optional) File extension.
*
* @return void
*/
public function js(
string $viewName,
?string $viewPath = null,
string $extension = 'js'
): void {
header("Content-type: application/javascript");
$this->include($viewName, $viewPath, $extension);
}
/**
* Prints data as JSON.
*
* @param object|array $data Object or array to be printed as JSON.
*
* @return void
*/
public function json(object|array $data): void
{
header('Content-Type: application/json; charset=utf-8');
print(json_encode($data));
}
/**
* Prints data as plain text.
*
* @param string $txt Text content.
*
* @return void
*/
public function text(string $txt): void
{
header('Content-Type: text/plain; charset=utf-8');
print($txt);
}
/**
* Attempts to return the absolute URL from a relative path.
*
* @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;
}
}
?>