Files
2026-09-01 10:46:29 -03:00

415 lines
14 KiB
PHP
Executable File

<?php
namespace Kernel;
use DI\Container;
use DI\ContainerBuilder;
use FastRoute\RouteCollector;
use Illuminate\Database\Capsule\Manager as Capsule;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use function FastRoute\simpleDispatcher;
class Application
{
protected static ?Application $instance = null;
protected Container $container;
protected Request $request;
protected array $routes = [];
protected ModuleLoader $moduleLoader;
protected $dispatcher;
protected HookManager $hooks;
protected array $publicPaths = [];
protected array $globalMiddlewares = [];
protected array $protectedMiddlewares = [];
public function __construct()
{
self::$instance = $this;
load_env(); // Uses the centralized helper
$this->initSession();
$this->initContainer();
$this->initDatabase();
$this->hooks = new HookManager();
$this->initModuleLoader();
$this->initTwig();
$this->initRouter();
}
/**
* Gets the application instance
*/
public static function getInstance(): ?Application
{
return self::$instance;
}
/**
* Returns the DI Container
*/
public function getContainer(): Container
{
return $this->container;
}
public function getHookManager(): HookManager
{
return $this->hooks;
}
/**
* Initializes the session
*/
protected function initSession(): void
{
// Session settings based on .env
$lifetime = $_ENV['SESSION_LIFETIME'] ?? 120;
$path = '/';
$domain = '';
$secure = false;
$httponly = true;
ini_set('session.gc_maxlifetime', $lifetime * 60);
ini_set('session.gc_probability', 1);
ini_set('session.gc_divisor', 100);
ini_set('session.use_strict_mode', '1');
// Use memory (RAM) for sessions via /dev/shm to avoid disk I/O and permission issues
$sessionPath = '/dev/shm';
// Fallback to system temp if /dev/shm doesn't exist (e.g. non-Linux environment)
if (!is_dir($sessionPath) || !is_writable($sessionPath)) {
$sessionPath = sys_get_temp_dir();
}
session_save_path($sessionPath);
session_set_cookie_params($lifetime * 60, $path, $domain, $secure, $httponly);
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
// Generate CSRF token if it doesn't exist
if (!isset($_SESSION['_token'])) {
$_SESSION['_token'] = bin2hex(random_bytes(32));
}
// Define default language if not set
if (!isset($_SESSION['lang'])) {
$_SESSION['lang'] = is_multi_lang_enabled() ? ($_ENV['APP_LANG_DEFAULT'] ?? 'pt_BR') : ($_ENV['APP_LANG_DEFAULT'] ?? 'pt_BR');
}
// Update last activity
$_SESSION['last_activity'] = time();
}
/**
* Creates the DI Container
*/
protected function initContainer(): void
{
$builder = new ContainerBuilder();
$builder->useAutowiring(true);
// $builder->useAnnotations(false); // Method removed in recent PHP-DI versions
// If you have definitions
if (file_exists(__DIR__ . '/../config/container.php')) {
$builder->addDefinitions(__DIR__ . '/../config/container.php');
}
$this->container = $builder->build();
$this->container->set(Container::class, $this->container);
}
/**
* Initializes the database with Illuminate Database
*/
protected function initDatabase(): void
{
$capsule = new Capsule;
$capsule->addConnection(get_db_config());
$capsule->setAsGlobal();
$capsule->bootEloquent();
}
/**
* Initializes the template system (Twig)
*/
protected function initTwig(): void
{
// Use Kernel\View to initialize the engine with theme support
$view = new View();
$twig = $view->getEngine();
// Add extra global functions if needed (most are now in Kernel\View)
// Add is_multi_lang_enabled function to templates
$twig->addFunction(new \Twig\TwigFunction('is_multi_lang_enabled', function () {
return ($_ENV['APP_MULTI_LANG'] ?? 'false') === 'true';
}));
// Add CSRF function
$twig->addFunction(new \Twig\TwigFunction('csrf_field', function () {
return '<input type="hidden" name="_token" value="' . ($_SESSION['_token'] ?? '') . '">';
}, ['is_safe' => ['html']]));
// Add old function to retrieve form values
$twig->addFunction(new \Twig\TwigFunction('old', function ($key, $default = '') {
return $_POST[$key] ?? $default;
}));
// Add session function to access session data
$twig->addFunction(new \Twig\TwigFunction('session', function ($key = null) {
if ($key === null) {
return $_SESSION;
}
return $_SESSION[$key] ?? null;
}));
// Add company_date filter
$twig->addFilter(new \Twig\TwigFilter('company_date', function ($date, $includeTime = false) {
return \App\Helpers\DateFormatter::format($date, $includeTime);
}));
}
/**
* Loads modules via ModuleLoader
*/
protected function initModuleLoader(): void
{
$this->moduleLoader = new ModuleLoader(dirname(__DIR__) . '/modules');
$this->moduleLoader->registerAutoload();
$this->moduleLoader->loadAll();
// Initialize translator after modules are loaded
$templatesPath = dirname(__DIR__) . '/templates/locales';
if (is_dir($templatesPath)) {
\Kernel\Translator::addNamespace('global', $templatesPath);
}
// Add theme locales if present (Parent Theme first, then Active Theme for overrides)
$parentTheme = \Kernel\View::getParentTheme();
if ($parentTheme) {
$parentLocalesPath = dirname(__DIR__) . '/themes/' . $parentTheme . '/locales';
if (is_dir($parentLocalesPath)) {
\Kernel\Translator::addThemePath($parentLocalesPath);
}
}
$activeTheme = \Kernel\View::getActiveTheme();
if ($activeTheme) {
$themeLocalesPath = dirname(__DIR__) . '/themes/' . $activeTheme . '/locales';
if (is_dir($themeLocalesPath)) {
\Kernel\Translator::addThemePath($themeLocalesPath);
}
}
$locale = $_SESSION['lang'] ?? ($_ENV['APP_LANG_DEFAULT'] ?? 'pt_BR');
\Kernel\Translator::setLocale($locale);
}
/**
* Initializes the routing system
*/
protected function initRouter(): void
{
// Load web routes
if (file_exists(__DIR__ . '/../routes/web.php')) {
$routeLoader = require __DIR__ . '/../routes/web.php';
$routeLoader($this);
}
$this->dispatcher = simpleDispatcher(function (RouteCollector $r) {
foreach ($this->routes as $route) {
[$method, $uri, $handler] = $route;
$r->addRoute($method, $uri, $handler);
}
});
// Debug
// $debugData = date('Y-m-d H:i:s') . " - Init Router with " . count($this->routes) . " routes\n";
// file_put_contents(__DIR__ . '/../debug_log.txt', $debugData, FILE_APPEND);
}
/**
* Allows modules to register routes
*/
public function addRoute(string $method, string $uri, $handler): void
{
$this->routes[] = [$method, $uri, $handler];
}
/**
* Returns all registered routes
*/
public function getRoutes(): array
{
return $this->routes;
}
/**
* Adds a public path (which does not require authentication)
*/
public function addPublicPath(string $path): void
{
$this->publicPaths[] = $path;
}
/**
* Checks if the path is public
*/
public function isPublicPath(string $path): bool
{
foreach ($this->publicPaths as $publicPath) {
if (str_ends_with($publicPath, '*')) {
$prefix = rtrim($publicPath, '*');
if (strpos($path, $prefix) === 0) {
return true;
}
} else {
if ($path === $publicPath) {
return true;
}
}
}
return false;
}
/**
* Adds a global middleware (executed on all requests)
*/
public function addGlobalMiddleware(object $middleware): void
{
$this->globalMiddlewares[] = $middleware;
}
/**
* Adds a middleware that will be executed on protected routes
*/
public function addProtectedMiddleware(object $middleware): void
{
$this->protectedMiddlewares[] = $middleware;
}
/**
* Runs the application
*/
public function run(): void
{
// Maintenance mode check
if (file_exists(dirname(__DIR__) . '/storage/framework/down')) {
$data = json_decode(file_get_contents(dirname(__DIR__) . '/storage/framework/down'), true);
$view = new \Kernel\View();
$response = new Response($view->render('errors/503.twig', [
'message' => $data['message'] ?? 'O sistema está em manutenção.',
'retry' => $data['retry'] ?? null
]), 503);
$response->send();
return;
}
Request::enableHttpMethodParameterOverride();
$this->request = Request::createFromGlobals();
// Standard Public Paths (Core)
$this->addPublicPath('/language/*');
$this->addPublicPath('/storage/*');
$this->addPublicPath('/captcha');
try {
$routeInfo = $this->dispatcher->dispatch(
$this->request->getMethod(),
$this->request->getPathInfo()
);
switch ($routeInfo[0]) {
case \FastRoute\Dispatcher::NOT_FOUND:
$view = new \Kernel\View();
$response = new Response($view->render('errors/404.twig'), 404);
break;
case \FastRoute\Dispatcher::METHOD_NOT_ALLOWED:
$response = new Response("405 - Method Not Allowed", 405);
break;
case \FastRoute\Dispatcher::FOUND:
$handler = $routeInfo[1];
$vars = $routeInfo[2];
// Apply Global Middlewares
foreach ($this->globalMiddlewares as $middleware) {
if (method_exists($middleware, 'handle')) {
$result = $middleware->handle($this->request, function ($req) {
return $req;
});
if ($result instanceof Response) {
$response = $result;
break 2; // Break out of switch
}
}
}
// Check Public Access
$path = $this->request->getPathInfo();
if (!$this->isPublicPath($path)) {
// Apply protected middlewares (e.g., Auth, Company)
foreach ($this->protectedMiddlewares as $middleware) {
if (method_exists($middleware, 'handle')) {
$result = $middleware->handle($this->request, function ($req) {
return $req;
});
if ($result instanceof Response) {
$response = $result;
break 2; // Break out of switch
}
}
}
}
// Execute the controller method
if (is_array($handler) && isset($handler[0], $handler[1])) {
$controller = $this->container->get($handler[0]);
$method = $handler[1];
$response = $controller->$method($this->request, ...array_values($vars));
} else {
$response = $handler($this->request, ...array_values($vars));
}
if (!$response instanceof Response) {
$response = new Response($response);
}
break;
}
} catch (\Throwable $e) {
// Log the error
if ($this->container->has('logging')) {
// $logger = $this->container->get('logging');
// $logger->error($e->getMessage(), ['trace' => $e->getTraceAsString()]);
}
error_log($e->getMessage() . "\n" . $e->getTraceAsString());
if (env('APP_DEBUG', false)) {
throw $e;
}
try {
$view = new \Kernel\View();
$response = new Response($view->render('errors/500.twig'), 500);
} catch (\Throwable $viewError) {
// Fallback if view rendering fails
$response = new Response("<h1>500 - Internal Server Error</h1><p>An unexpected error occurred.</p>", 500);
}
}
$response->send();
}
}