Compare commits
20
Commits
b95747bfe0
..
v0.1.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3b6da45b01 | ||
|
|
b7260b5199 | ||
|
|
1f29622914 | ||
|
|
eb882db0c8 | ||
|
|
9ba43e3f25 | ||
|
|
54e96ba4bb | ||
|
|
9027f36748 | ||
|
|
2ae9ef39f3 | ||
|
|
ee0db0307e | ||
|
|
44cbe6bbdc | ||
|
|
31728af351 | ||
|
|
4845d14ef5 | ||
|
|
aca4e732ed | ||
|
|
f475c9125f | ||
|
|
4fe7314389 | ||
|
|
1f366830bc | ||
|
|
15df7c8d96 | ||
|
|
b8dd1fd0f6 | ||
|
|
83c80bd4ab | ||
|
|
b13c07d3c4 |
@@ -12,8 +12,3 @@ spl_autoload_register(function ($className) {
|
||||
require_once $file;
|
||||
}
|
||||
});
|
||||
|
||||
set_exception_handler(function ($exception) {
|
||||
echo "Uncaught exception: " , $exception->getMessage(), "\n";
|
||||
echo $exception->getTraceAsString();
|
||||
});
|
||||
|
||||
+26
-16
@@ -31,7 +31,7 @@ class Model
|
||||
/**
|
||||
* @var array Attributes to ignore when saving to the database.
|
||||
*/
|
||||
protected static array $dbIgnoreSave = ['id'];
|
||||
protected static array $dbIgnoreSave = [];
|
||||
|
||||
/**
|
||||
* @var array Attributes that should be explicitly saved, even if private/protected.
|
||||
@@ -176,6 +176,10 @@ class Model
|
||||
|
||||
$vars = json_encode(static::$dbQueryVariables);
|
||||
|
||||
if ($resetQuery) {
|
||||
static::resetQuery();
|
||||
}
|
||||
|
||||
throw new Exception(
|
||||
"\nError at query to database.\n" .
|
||||
"Query: $query\n" .
|
||||
@@ -480,12 +484,8 @@ class Model
|
||||
|
||||
$set = [];
|
||||
foreach ($atts as $key => $value) {
|
||||
if ($value !== null) {
|
||||
$set[] = "$key=:$key";
|
||||
static::$dbQueryVariables[':' . $key] = $value;
|
||||
} else {
|
||||
$set[] = "$key=NULL";
|
||||
}
|
||||
$set[] = "$key=:$key";
|
||||
static::$dbQueryVariables[':' . $key] = $value;
|
||||
}
|
||||
|
||||
$table = static::table();
|
||||
@@ -511,11 +511,9 @@ class Model
|
||||
static::$dbQueryVariables = [];
|
||||
|
||||
foreach ($atts as $key => $value) {
|
||||
if (isset($value)) {
|
||||
$into[] = "$key";
|
||||
$values[] = ":$key";
|
||||
static::$dbQueryVariables[":$key"] = $value;
|
||||
}
|
||||
$into[] = "$key";
|
||||
$values[] = ":$key";
|
||||
static::$dbQueryVariables[":$key"] = $value;
|
||||
}
|
||||
|
||||
$table = static::table();
|
||||
@@ -523,7 +521,7 @@ class Model
|
||||
static::query($sql);
|
||||
|
||||
$pk = static::$dbPrimaryKey;
|
||||
$this->$pk = $db->lastInsertId();
|
||||
$this->$pk ??= $db->lastInsertId();
|
||||
$this->markAsSaved();
|
||||
}
|
||||
|
||||
@@ -1014,7 +1012,9 @@ class Model
|
||||
public static function orderBy(string $value, string $order = 'ASC'): static
|
||||
{
|
||||
if ($value == "RAND") {
|
||||
static::$dbQuery['orderBy'] = 'RAND()';
|
||||
$random = static::db()->getAttribute(PDO::ATTR_DRIVER_NAME) == 'mysql' ? 'RAND()' : 'RANDOM()';
|
||||
static::$dbQuery['orderBy'] = $random;
|
||||
|
||||
return new static();
|
||||
}
|
||||
|
||||
@@ -1104,8 +1104,18 @@ class Model
|
||||
public static function search(string $search, ?array $in = null): static
|
||||
{
|
||||
if ($in == null) {
|
||||
$className = get_called_class();
|
||||
$in = array_keys((new $className())->getVars());
|
||||
$in = [];
|
||||
$reflection = new \ReflectionClass(get_called_class());
|
||||
|
||||
foreach ($reflection->getProperties(\ReflectionProperty::IS_PUBLIC) as $property) {
|
||||
if (!in_array($property->getName(), static::$dbIgnoreSave)) {
|
||||
$in[] = static::camelCaseToSnakeCase($property->getName());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (empty($in)) {
|
||||
return new static();
|
||||
}
|
||||
|
||||
$search = static::bind($search);
|
||||
|
||||
+48
-17
@@ -2,6 +2,8 @@
|
||||
|
||||
namespace Libs;
|
||||
|
||||
use Exception;
|
||||
|
||||
/**
|
||||
* Request - DuckBrain
|
||||
*
|
||||
@@ -59,39 +61,48 @@ class Request extends Neuron
|
||||
}
|
||||
|
||||
// Run configured validations
|
||||
if (!$this->validate()) {
|
||||
exit();
|
||||
}
|
||||
$this->validate();
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts the configured validation.
|
||||
*
|
||||
* @return bool
|
||||
* 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(): bool
|
||||
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(), $actual)
|
||||
Validator::validateList(static::rules(), $body)
|
||||
) {
|
||||
return true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (isset(static::messages()[Validator::$lastFailed])) {
|
||||
$error = static::messages()[Validator::$lastFailed];
|
||||
} else {
|
||||
$error = 'Error: validation failed of ' . preg_replace('/\./', ' as ', Validator::$lastFailed, 1);
|
||||
}
|
||||
$error = Validator::message(
|
||||
Validator::$lastFailed,
|
||||
static::messages(),
|
||||
static::attributes()
|
||||
);
|
||||
|
||||
static::onInvalid($error);
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -134,16 +145,36 @@ class Request extends Neuron
|
||||
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 void
|
||||
* @return never
|
||||
* @throws Exception
|
||||
*/
|
||||
public function onInvalid(string $error): void
|
||||
public function onInvalid(string $error): never
|
||||
{
|
||||
http_response_code(422);
|
||||
print($error);
|
||||
throw new Exception($error, 422);
|
||||
}
|
||||
}
|
||||
|
||||
+90
-33
@@ -50,7 +50,7 @@ class Router
|
||||
*
|
||||
* @var callable $notFoundCallback
|
||||
*/
|
||||
public static $notFoundCallback = 'Libs\Router::defaultNotFound';
|
||||
public static $notFoundCallback = 'Libs\Router::defaultNotFound';
|
||||
|
||||
/**
|
||||
* Default callback function for when
|
||||
@@ -64,6 +64,54 @@ class Router
|
||||
echo '<h2 style="text-align: center;margin: 25px 0px;">Error 404 - Page Not Found</h2>';
|
||||
}
|
||||
|
||||
/**
|
||||
* 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';
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
@@ -136,7 +184,6 @@ class Router
|
||||
public static function redirect(string $path): void
|
||||
{
|
||||
header('Location: ' . static::basePath() . ltrim($path, '/'));
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -363,50 +410,60 @@ class Router
|
||||
/**
|
||||
* Applies the route configuration.
|
||||
*
|
||||
* 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).
|
||||
*
|
||||
* @param string|null $path (optional) Path to use. If not defined, it detects the current path.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function apply(?string $path = null): void
|
||||
{
|
||||
$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
|
||||
};
|
||||
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
|
||||
};
|
||||
|
||||
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) { // 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]);
|
||||
|
||||
// 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]);
|
||||
// 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);
|
||||
}
|
||||
// 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));
|
||||
}
|
||||
// By default, prints as JSON if something is returned
|
||||
if (isset($data)) {
|
||||
header('Content-Type: application/json');
|
||||
print(json_encode($data));
|
||||
}
|
||||
|
||||
return;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If no router matches, call $notFoundCallBack
|
||||
Synapsis::resolve(static::$notFoundCallback);
|
||||
// If no router matches, call $notFoundCallBack
|
||||
Synapsis::resolve(static::$notFoundCallback);
|
||||
} catch (\Throwable $exception) {
|
||||
Synapsis::resolve(static::$exceptionCallback, ['exception' => $exception]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+15
-4
@@ -65,12 +65,14 @@ class Synapsis
|
||||
/**
|
||||
* Resolves and injects dependencies for a callable and returns its result.
|
||||
*
|
||||
* @param callable $action
|
||||
* @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): mixed
|
||||
public static function resolve(callable $action, array $named = []): mixed
|
||||
{
|
||||
if ($action instanceof Closure) { // If it's an anonymous function
|
||||
$reflectionCallback = new ReflectionFunction($action);
|
||||
@@ -90,7 +92,7 @@ class Synapsis
|
||||
// Get the parameters
|
||||
return call_user_func_array(
|
||||
$action,
|
||||
static::resolveParameterValues($reflectionCallback->getParameters())
|
||||
static::resolveParameterValues($reflectionCallback->getParameters(), $named)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -132,11 +134,15 @@ class Synapsis
|
||||
* 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
|
||||
public static function resolveParameterValues(array $parameters, array $named = []): array
|
||||
{
|
||||
$values = [];
|
||||
foreach ($parameters as $parameter) {
|
||||
@@ -144,6 +150,11 @@ class Synapsis
|
||||
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;
|
||||
|
||||
+1075
-32
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user