Commit 2ed51510 authored by Administrator's avatar Administrator

Update 4 files via Son of Anton

parent ee11d13c
Pipeline #20 canceled with stage
......@@ -120,56 +120,83 @@ $container->singleton(JobRunner::class, function () use ($container) {
return new JobRunner($container->resolve(Connection::class));
});
// Router
$container->singleton(Router::class, fn() => new Router());
// Router — use the static singleton so route files can call Router::get() etc.
$container->singleton(Router::class, fn() => Router::getInstance());
// App
$container->singleton(App::class, function () use ($container) {
return new App($container);
});
// Register Event Listeners
// ─── Register Event Listeners ───
$dispatcher = $container->resolve(EventDispatcher::class);
$listenerRegistrar = ROOT_PATH . '/config/event_listeners.php';
if (file_exists($listenerRegistrar)) {
$listeners = require $listenerRegistrar;
foreach ($listeners as $event => $handlers) {
foreach ($handlers as $handler) {
$dispatcher->listen($event, function ($payload) use ($handler, $container) {
$instance = is_string($handler) ? $container->resolve($handler) : $handler;
$instance->handle($payload);
});
if (is_array($listeners)) {
foreach ($listeners as $event => $handlers) {
foreach ($handlers as $handler) {
$dispatcher->listen($event, function ($payload) use ($handler, $container) {
$instance = is_string($handler) ? $container->resolve($handler) : $handler;
$instance->handle($payload);
});
}
}
}
}
// Register Calculators
// ─── Register Calculators (silently skip if classes don't exist) ───
$calcEngine = $container->resolve(CalculationEngine::class);
$calculators = ROOT_PATH . '/config/calculators.php';
if (file_exists($calculators)) {
$calcs = require $calculators;
$calculatorsFile = ROOT_PATH . '/config/calculators.php';
if (file_exists($calculatorsFile)) {
$calcs = require $calculatorsFile;
foreach ($calcs as $name => $className) {
$calcEngine->register($name, $container->resolve($className));
try {
if (class_exists($className)) {
$calcEngine->register($name, $container->resolve($className));
}
} catch (\Throwable $e) {
error_log("[Bootstrap] Failed to register calculator '{$name}': {$e->getMessage()}");
}
}
}
// Register Scheduled Jobs
// ─── Register Scheduled Jobs (silently skip if classes don't exist) ───
$jobRunner = $container->resolve(JobRunner::class);
$jobs = ROOT_PATH . '/config/scheduled_jobs.php';
if (file_exists($jobs)) {
$jobDefs = require $jobs;
$jobsFile = ROOT_PATH . '/config/scheduled_jobs.php';
if (file_exists($jobsFile)) {
$jobDefs = require $jobsFile;
foreach ($jobDefs as $key => $className) {
$jobRunner->register($key, $container->resolve($className));
try {
if (class_exists($className)) {
$jobRunner->register($key, $container->resolve($className));
}
} catch (\Throwable $e) {
error_log("[Bootstrap] Failed to register job '{$key}': {$e->getMessage()}");
}
}
}
// Load all module routes
// ─── Load All Module Routes ───
$router = $container->resolve(Router::class);
$moduleDir = ROOT_PATH . '/modules';
$modules = array_filter(glob($moduleDir . '/*'), 'is_dir');
foreach ($modules as $modulePath) {
$routeFile = $modulePath . '/routes.php';
if (file_exists($routeFile)) {
require $routeFile;
if (is_dir($moduleDir)) {
$modules = array_filter(glob($moduleDir . '/*'), 'is_dir');
sort($modules);
foreach ($modules as $modulePath) {
$routeFile = $modulePath . '/routes.php';
if (file_exists($routeFile)) {
try {
$result = require $routeFile;
// Handle route files that return a callable (BoardTemplates, CardTemplates pattern)
if (is_callable($result)) {
$result($router);
}
} catch (\Throwable $e) {
$moduleName = basename($modulePath);
error_log("[Bootstrap] Failed to load routes for module '{$moduleName}': {$e->getMessage()} in {$e->getFile()}:{$e->getLine()}");
}
}
}
}
\ No newline at end of file
......@@ -36,10 +36,17 @@ final class App
$router = $this->container->resolve(Router::class);
try {
// Handle root redirect to /login
if ($request->uri() === '/' || $request->uri() === '') {
$response = Response::redirect('/login');
$response->send();
return;
}
$route = $router->match($request);
if ($route === null) {
$this->sendError(404, 'Not Found');
$this->sendError(404, 'Not Found', $request);
return;
}
......@@ -55,14 +62,9 @@ final class App
$response->send();
} catch (\Throwable $e) {
$debug = $this->container->resolve(Config::class)->get('app.debug', false);
error_log("[App Error] {$e->getMessage()} in {$e->getFile()}:{$e->getLine()}");
if ($debug) {
$this->sendError(500, $e->getMessage() . "\n" . $e->getTraceAsString());
} else {
$this->sendError(500, 'Internal Server Error');
}
$this->sendError(500, $e->getMessage(), $request);
}
}
......@@ -72,10 +74,14 @@ final class App
$method = $route['method'];
$params = $route['params'] ?? [];
if (!class_exists($controllerClass)) {
return Response::json(['error' => "Controller not found: {$controllerClass}"], 500);
}
$controller = $this->container->resolve($controllerClass);
if (!method_exists($controller, $method)) {
return Response::json(['error' => 'Method not found'], 404);
return Response::json(['error' => "Method {$method} not found on {$controllerClass}"], 404);
}
$result = $controller->$method($request, ...$params);
......@@ -87,20 +93,52 @@ final class App
return Response::json($result);
}
private function sendError(int $code, string $message): void
private function sendError(int $code, string $message, ?Request $request = null): void
{
http_response_code($code);
if (str_contains($_SERVER['HTTP_ACCEPT'] ?? '', 'application/json')) {
$wantsJson = $request && (
str_contains($request->header('accept', ''), 'json') ||
str_contains($request->header('content-type', ''), 'json') ||
str_starts_with($request->uri(), '/api/')
);
if ($wantsJson) {
header('Content-Type: application/json');
echo json_encode(['error' => $message, 'code' => $code]);
} else {
$templateEngine = $this->container->resolve(\Engine\Template\TemplateEngine::class);
$templateFile = ROOT_PATH . "/templates/errors/{$code}.php";
if (file_exists($templateFile)) {
echo $templateEngine->render("errors/{$code}", ['message' => $message]);
} else {
echo "<h1>{$code}</h1><p>" . htmlspecialchars($message) . "</p>";
// Try template, fall back to inline HTML
try {
$templateEngine = $this->container->resolve(\Engine\Template\TemplateEngine::class);
$templateFile = ROOT_PATH . "/templates/errors/{$code}.php";
if (file_exists($templateFile)) {
echo $templateEngine->render("errors/{$code}", ['message' => $message]);
return;
}
} catch (\Throwable $e) {
// Template engine failed, use inline HTML
}
echo <<<HTML
<!DOCTYPE html>
<html>
<head><title>{$code} Error</title>
<style>
body{font-family:-apple-system,sans-serif;display:flex;align-items:center;justify-content:center;min-height:100vh;margin:0;background:#0f172a;color:#e2e8f0}
.box{text-align:center;max-width:600px;padding:40px}
h1{font-size:5em;margin:0;color:#6366f1}
p{color:#94a3b8;margin-top:16px}
pre{background:#1e293b;padding:16px;border-radius:8px;text-align:left;overflow-x:auto;font-size:0.85em;margin-top:20px;white-space:pre-wrap}
a{color:#6366f1;text-decoration:none}
</style>
</head>
<body><div class="box">
<h1>{$code}</h1>
<p>{$message}</p>
<pre>{$message}</pre>
<p style="margin-top:24px"><a href="/login">← Go to Login</a></p>
</div></body></html>
HTML;
}
}
}
\ No newline at end of file
......@@ -5,37 +5,116 @@ namespace Engine\Core;
final class Router
{
private static ?self $instance = null;
private array $routes = [];
private array $groupStack = [];
public function get(string $uri, string $controller, string $method): self
/**
* Middleware alias map — short names to full class names.
*/
private static array $middlewareAliases = [
'auth' => \Middleware\AuthenticationMiddleware::class,
'audit' => \Middleware\AuditMiddleware::class,
'json_body' => \Middleware\JsonBodyParserMiddleware::class,
'api_key_auth' => \Middleware\ApiKeyAuthMiddleware::class,
'cors' => \Middleware\CORSMiddleware::class,
'csrf' => \Middleware\CSRFMiddleware::class,
'blocking' => \Middleware\BlockingNotificationMiddleware::class,
'security' => \Middleware\SecurityHeadersMiddleware::class,
];
public function __construct()
{
return $this->addRoute('GET', $uri, $controller, $method);
if (self::$instance === null) {
self::$instance = $this;
}
}
public static function getInstance(): self
{
if (self::$instance === null) {
self::$instance = new self();
}
return self::$instance;
}
/**
* Resolve middleware alias to full class name.
*/
public static function resolveMiddleware(string $middleware): string
{
return self::$middlewareAliases[$middleware] ?? $middleware;
}
public function post(string $uri, string $controller, string $method): self
// ─── Static Route Registration (supports both calling patterns) ───
/**
* Supports:
* Router::get('/path', [Controller::class, 'method'])
* Router::get('/path', Controller::class, 'method')
*/
public static function get(string $uri, $handler, ?string $action = null): self
{
return $this->addRoute('POST', $uri, $controller, $method);
return self::getInstance()->addRoute('GET', $uri, $handler, $action);
}
public function put(string $uri, string $controller, string $method): self
public static function post(string $uri, $handler, ?string $action = null): self
{
return $this->addRoute('PUT', $uri, $controller, $method);
return self::getInstance()->addRoute('POST', $uri, $handler, $action);
}
public function delete(string $uri, string $controller, string $method): self
public static function put(string $uri, $handler, ?string $action = null): self
{
return $this->addRoute('DELETE', $uri, $controller, $method);
return self::getInstance()->addRoute('PUT', $uri, $handler, $action);
}
public function group(array $attributes, callable $callback): void
public static function delete(string $uri, $handler, ?string $action = null): self
{
$this->groupStack[] = $attributes;
$callback($this);
array_pop($this->groupStack);
return self::getInstance()->addRoute('DELETE', $uri, $handler, $action);
}
public function middleware(array|string $middleware): self
/**
* Route group. Supports both calling patterns:
*
* Router::group('/prefix', ['middleware' => ['auth']], function() { ... })
* $router->group(['prefix' => '/x', 'middleware' => [Class::class]], function($r) { ... })
*/
public static function group($prefixOrAttributes, $attributesOrCallback = null, $callback = null): void
{
$instance = self::getInstance();
if (is_string($prefixOrAttributes)) {
// Pattern: group('/prefix', ['middleware' => [...]], callable)
// OR: group('/prefix', callable)
if (is_callable($attributesOrCallback) && $callback === null) {
$attributes = ['prefix' => $prefixOrAttributes];
$cb = $attributesOrCallback;
} else {
$attributes = is_array($attributesOrCallback) ? $attributesOrCallback : [];
$attributes['prefix'] = $prefixOrAttributes;
$cb = $callback;
}
} elseif (is_array($prefixOrAttributes)) {
// Pattern: group(['prefix' => '/x', 'middleware' => [...]], callable)
$attributes = $prefixOrAttributes;
$cb = $attributesOrCallback;
} else {
return;
}
$instance->groupStack[] = $attributes;
if (is_callable($cb)) {
$cb($instance);
}
array_pop($instance->groupStack);
}
/**
* Chainable middleware on last registered route.
*/
public function middleware($middleware): self
{
$last = array_key_last($this->routes);
if ($last !== null) {
......@@ -48,8 +127,23 @@ final class Router
return $this;
}
private function addRoute(string $httpMethod, string $uri, string $controller, string $action): self
// ─── Internal Route Registration ───
private function addRoute(string $httpMethod, string $uri, $handler, ?string $action = null): self
{
// Handle [Controller::class, 'method'] array syntax
if (is_array($handler)) {
$controller = $handler[0];
$action = $handler[1] ?? $action;
} else {
$controller = $handler;
}
if (!$action) {
throw new \RuntimeException("Route {$httpMethod} {$uri}: no action/method specified.");
}
// Build prefix and middleware from group stack
$prefix = '';
$middleware = [];
......@@ -63,6 +157,9 @@ final class Router
}
}
// Resolve middleware aliases to full class names
$middleware = array_map([self::class, 'resolveMiddleware'], $middleware);
$fullUri = $prefix . '/' . ltrim($uri, '/');
$fullUri = '/' . trim($fullUri, '/');
if ($fullUri !== '/') {
......@@ -114,4 +211,9 @@ final class Router
return null;
}
public function getRoutes(): array
{
return $this->routes;
}
}
\ No newline at end of file
......@@ -3,11 +3,32 @@ declare(strict_types=1);
define('ROOT_PATH', dirname(__DIR__));
// Show errors during development
error_reporting(E_ALL);
ini_set('display_errors', '0');
ini_set('log_errors', '1');
ini_set('error_log', ROOT_PATH . '/storage/logs/php-error.log');
require ROOT_PATH . '/bootstrap/autoload.php';
require ROOT_PATH . '/bootstrap/app.php';
use Engine\Core\App;
use Engine\Core\Container;
try {
require ROOT_PATH . '/bootstrap/app.php';
$app = \Engine\Core\Container::getInstance()->resolve(\Engine\Core\App::class);
$app->run();
} catch (\Throwable $e) {
error_log("[FATAL] {$e->getMessage()} in {$e->getFile()}:{$e->getLine()}\n{$e->getTraceAsString()}");
$app = Container::getInstance()->resolve(App::class);
$app->run();
\ No newline at end of file
http_response_code(500);
if (str_contains($_SERVER['HTTP_ACCEPT'] ?? '', 'json')) {
header('Content-Type: application/json');
echo json_encode(['error' => 'Internal Server Error', 'message' => $e->getMessage(), 'file' => $e->getFile(), 'line' => $e->getLine()]);
} else {
echo '<!DOCTYPE html><html><head><title>500</title><style>body{font-family:monospace;padding:40px;background:#0f172a;color:#e2e8f0}h1{color:#ef4444}pre{background:#1e293b;padding:20px;border-radius:8px;overflow-x:auto;white-space:pre-wrap}</style></head><body>';
echo '<h1>500 — Server Error</h1>';
echo '<pre>' . htmlspecialchars($e->getMessage()) . "\n\n";
echo htmlspecialchars($e->getFile()) . ':' . $e->getLine() . "\n\n";
echo htmlspecialchars($e->getTraceAsString());
echo '</pre></body></html>';
}
}
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment