First Commit
This commit is contained in:
Executable
+414
@@ -0,0 +1,414 @@
|
||||
<?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();
|
||||
}
|
||||
}
|
||||
Executable
+47
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
namespace Kernel;
|
||||
|
||||
class HookManager
|
||||
{
|
||||
protected array $actions = [];
|
||||
protected array $filters = [];
|
||||
|
||||
public function addAction(string $hook, callable $listener, int $priority = 10): void
|
||||
{
|
||||
$this->actions[$hook][$priority][] = $listener;
|
||||
}
|
||||
|
||||
public function doAction(string $hook, ...$args): void
|
||||
{
|
||||
if (!isset($this->actions[$hook])) {
|
||||
return;
|
||||
}
|
||||
ksort($this->actions[$hook]);
|
||||
foreach ($this->actions[$hook] as $listeners) {
|
||||
foreach ($listeners as $listener) {
|
||||
$listener(...$args);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function addFilter(string $hook, callable $listener, int $priority = 10): void
|
||||
{
|
||||
$this->filters[$hook][$priority][] = $listener;
|
||||
}
|
||||
|
||||
public function applyFilters(string $hook, $value, ...$args)
|
||||
{
|
||||
if (!isset($this->filters[$hook])) {
|
||||
return $value;
|
||||
}
|
||||
ksort($this->filters[$hook]);
|
||||
foreach ($this->filters[$hook] as $listeners) {
|
||||
foreach ($listeners as $listener) {
|
||||
$value = $listener($value, ...$args);
|
||||
}
|
||||
}
|
||||
return $value;
|
||||
}
|
||||
}
|
||||
|
||||
Executable
+62
@@ -0,0 +1,62 @@
|
||||
<?php
|
||||
|
||||
namespace Kernel;
|
||||
|
||||
/**
|
||||
* SysCMS - Content Management System
|
||||
*
|
||||
* Copyright (c) 2025 Marco Costa
|
||||
* All rights reserved.
|
||||
*
|
||||
* Author: Marco Costa
|
||||
* Email: contato@syspanel.com.br
|
||||
* Project: SysCMS
|
||||
*
|
||||
* This software is licensed, not sold.
|
||||
* Permission is granted to use this software
|
||||
* subject to the terms of the SysCMS Commercial License.
|
||||
*
|
||||
* Redistribution, sublicensing, resale, or
|
||||
* disclosure of this software, in whole or in part,
|
||||
* is strictly prohibited without prior written permission.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND.
|
||||
*/
|
||||
|
||||
class License
|
||||
{
|
||||
/**
|
||||
* Checks the license and terminates the application if invalid.
|
||||
*/
|
||||
public static function checkGlobalBlock(): void
|
||||
{
|
||||
// No restriction
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the system allows creating new companies.
|
||||
* Always returns true.
|
||||
*/
|
||||
public static function canCreateCompany(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current operation mode: 'mono' or 'multi'.
|
||||
* Always returns 'multi'.
|
||||
*/
|
||||
public static function getMode(): string
|
||||
{
|
||||
return 'multi';
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the license is valid for the current domain
|
||||
* Always returns true.
|
||||
*/
|
||||
public static function isValidDomain(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
Executable
+201
@@ -0,0 +1,201 @@
|
||||
<?php
|
||||
|
||||
namespace Kernel;
|
||||
|
||||
use Exception;
|
||||
|
||||
class ModuleLoader
|
||||
{
|
||||
protected string $modulesPath;
|
||||
protected array $modules = [];
|
||||
protected array $loaded = [];
|
||||
|
||||
public function __construct(string $modulesPath)
|
||||
{
|
||||
$this->modulesPath = rtrim($modulesPath, '/');
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers the autoloader for module classes
|
||||
* Pattern: Modules\{Module}\{Class} -> modules/{module}/classes/{Class}.php
|
||||
*/
|
||||
public function registerAutoload(): void
|
||||
{
|
||||
spl_autoload_register(function ($class) {
|
||||
// Base prefix
|
||||
$prefix = 'Modules\\';
|
||||
|
||||
// Checks if the class uses the prefix
|
||||
$len = strlen($prefix);
|
||||
if (strncmp($prefix, $class, $len) !== 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Gets the relative class name without the prefix
|
||||
$relativeClass = substr($class, $len);
|
||||
|
||||
// Separates the module name (first part of the namespace)
|
||||
$parts = explode('\\', $relativeClass);
|
||||
$moduleName = array_shift($parts);
|
||||
|
||||
if (empty($moduleName)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Converts the module name to lowercase to match the folder
|
||||
$moduleDir = strtolower($moduleName);
|
||||
|
||||
// Rebuilds the class path
|
||||
$classPath = implode('/', $parts);
|
||||
|
||||
// File path
|
||||
$file = $this->modulesPath . '/' . $moduleDir . '/classes/' . $classPath . '.php';
|
||||
|
||||
// If the file exists, load it
|
||||
if (file_exists($file)) {
|
||||
require $file;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads all modules
|
||||
*/
|
||||
public function loadAll(): void
|
||||
{
|
||||
$dirs = glob($this->modulesPath . '/*', GLOB_ONLYDIR);
|
||||
|
||||
foreach ($dirs as $dir) {
|
||||
$moduleName = basename($dir);
|
||||
$this->load($moduleName);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads a specific module
|
||||
*/
|
||||
public function load(string $moduleName): void
|
||||
{
|
||||
if (isset($this->loaded[$moduleName])) {
|
||||
return; // already loaded
|
||||
}
|
||||
|
||||
$modulePath = $this->modulesPath . '/' . $moduleName;
|
||||
|
||||
// Checks if it exists
|
||||
if (!is_dir($modulePath)) {
|
||||
throw new Exception("Módulo '{$moduleName}' não encontrado.");
|
||||
}
|
||||
|
||||
// Checks module.json
|
||||
$jsonFile = $modulePath . '/module.json';
|
||||
|
||||
if (!file_exists($jsonFile)) {
|
||||
throw new Exception("O módulo '{$moduleName}' não possui um module.json válido.");
|
||||
}
|
||||
|
||||
$config = json_decode(file_get_contents($jsonFile), true);
|
||||
|
||||
if (!is_array($config)) {
|
||||
throw new Exception("module.json inválido no módulo '{$moduleName}'.");
|
||||
}
|
||||
|
||||
$this->modules[$moduleName] = $config;
|
||||
|
||||
// Load Service Providers
|
||||
$this->registerProviders($config);
|
||||
|
||||
// Load Routes
|
||||
$this->registerRoutes($moduleName, $modulePath, $config);
|
||||
|
||||
// Register Views and Locales
|
||||
$this->registerResources($moduleName, $modulePath, $config);
|
||||
|
||||
$this->loaded[$moduleName] = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers Service Providers
|
||||
*/
|
||||
protected function registerProviders(array $config): void
|
||||
{
|
||||
if (!isset($config['providers'])) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($config['providers'] as $class) {
|
||||
if (class_exists($class)) {
|
||||
$provider = new $class();
|
||||
if (method_exists($provider, 'register')) {
|
||||
$provider->register();
|
||||
}
|
||||
if (method_exists($provider, 'boot')) {
|
||||
$provider->boot();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads route files
|
||||
*/
|
||||
protected function registerRoutes(string $module, string $path, array $config): void
|
||||
{
|
||||
if (!isset($config['routes'])) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the application instance to register routes
|
||||
$app = \Kernel\Application::getInstance();
|
||||
|
||||
if (isset($config['routes']['web'])) {
|
||||
$file = $path . '/' . $config['routes']['web'];
|
||||
if (file_exists($file)) {
|
||||
$routeLoader = require $file;
|
||||
if (is_callable($routeLoader) && $app) {
|
||||
$routeLoader($app);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($config['routes']['api'])) {
|
||||
$file = $path . '/' . $config['routes']['api'];
|
||||
if (file_exists($file)) {
|
||||
$routeLoader = require $file;
|
||||
if (is_callable($routeLoader) && $app) {
|
||||
$routeLoader($app);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers views and translations
|
||||
*/
|
||||
protected function registerResources(string $module, string $path, array $config): void
|
||||
{
|
||||
// Views
|
||||
if (isset($config['views'])) {
|
||||
$viewsPath = $path . '/' . $config['views'];
|
||||
if (is_dir($viewsPath)) {
|
||||
View::addPath($module, $viewsPath);
|
||||
}
|
||||
}
|
||||
|
||||
// Locales
|
||||
if (isset($config['locales'])) {
|
||||
$localesPath = $path . '/' . $config['locales'];
|
||||
if (is_dir($localesPath)) {
|
||||
Translator::addNamespace($module, $localesPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the list of loaded modules
|
||||
*/
|
||||
public function getModules(): array
|
||||
{
|
||||
return $this->modules;
|
||||
}
|
||||
}
|
||||
Executable
+166
@@ -0,0 +1,166 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* SysCMS - Content Management System
|
||||
*
|
||||
* Copyright (c) 2025 Marco Costa
|
||||
* All rights reserved.
|
||||
*
|
||||
* Author: Marco Costa
|
||||
* Email: contato@syspanel.com.br
|
||||
* Project: SysCMS
|
||||
*
|
||||
* This software is licensed, not sold.
|
||||
* Permission is granted to use this software
|
||||
* subject to the terms of the SysCMS Commercial License.
|
||||
*
|
||||
* Redistribution, sublicensing, resale, or
|
||||
* disclosure of this software, in whole or in part,
|
||||
* is strictly prohibited without prior written permission.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND.
|
||||
*/
|
||||
|
||||
namespace Kernel;
|
||||
|
||||
class Translator
|
||||
{
|
||||
protected static array $translations = [];
|
||||
protected static array $paths = [];
|
||||
protected static array $themePaths = [];
|
||||
protected static string $locale = 'pt_BR';
|
||||
|
||||
/**
|
||||
* Add a namespace for translations
|
||||
*/
|
||||
public static function addNamespace(string $namespace, string $path): void
|
||||
{
|
||||
self::$paths[$namespace] = $path;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a theme path for translation overrides
|
||||
*/
|
||||
public static function addThemePath(string $path): void
|
||||
{
|
||||
self::$themePaths[] = $path;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the current locale
|
||||
*/
|
||||
public static function setLocale(string $locale): void
|
||||
{
|
||||
self::$locale = $locale;
|
||||
self::loadTranslations();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current locale
|
||||
*/
|
||||
public static function getLocale(): string
|
||||
{
|
||||
return self::$locale;
|
||||
}
|
||||
|
||||
/**
|
||||
* Translate a key
|
||||
*/
|
||||
public static function translate(string $key, array $params = []): string
|
||||
{
|
||||
if (!isset(self::$translations[$key])) {
|
||||
return $key;
|
||||
}
|
||||
|
||||
$translation = self::$translations[$key];
|
||||
|
||||
// Se não foi passado platformName explicitamente, tenta pegar do PlatformService
|
||||
if (!isset($params['platformName'])) {
|
||||
try {
|
||||
$platformService = new \App\Services\PlatformService();
|
||||
$params['platformName'] = $platformService->getPlatformName();
|
||||
} catch (\Exception $e) {
|
||||
$params['platformName'] = 'SysCMS';
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($params)) {
|
||||
foreach ($params as $param => $value) {
|
||||
if ($value !== null) {
|
||||
$translation = str_replace(":{$param}", (string)$value, $translation);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $translation;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load translations from files
|
||||
*/
|
||||
protected static function loadTranslations(): void
|
||||
{
|
||||
self::$translations = [];
|
||||
|
||||
// Load standard paths
|
||||
foreach (self::$paths as $namespace => $path) {
|
||||
self::loadFromPath($path);
|
||||
}
|
||||
|
||||
// Load theme overrides (highest priority)
|
||||
foreach (self::$themePaths as $path) {
|
||||
self::loadFromPath($path);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load translations from a specific path
|
||||
*/
|
||||
protected static function loadFromPath(string $path): void
|
||||
{
|
||||
$localePath = $path . '/' . self::$locale;
|
||||
|
||||
if (is_dir($localePath)) {
|
||||
$files = glob($localePath . '/*.json');
|
||||
|
||||
foreach ($files as $file) {
|
||||
$filename = basename($file, '.json');
|
||||
$content = file_get_contents($file);
|
||||
if ($content === false) {
|
||||
continue;
|
||||
}
|
||||
$translations = json_decode($content, true);
|
||||
|
||||
if (is_array($translations)) {
|
||||
// Flatten the translations to support dot notation
|
||||
$flatTranslations = self::flatten($translations);
|
||||
foreach ($flatTranslations as $key => $value) {
|
||||
self::$translations[$filename . '.' . $key] = $value;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Flatten a multi-dimensional associative array with dot notation.
|
||||
*
|
||||
* @param array $array
|
||||
* @param string $prepend
|
||||
* @return array
|
||||
*/
|
||||
protected static function flatten(array $array, string $prepend = ''): array
|
||||
{
|
||||
$results = [];
|
||||
|
||||
foreach ($array as $key => $value) {
|
||||
if (is_array($value) && !empty($value)) {
|
||||
$results = array_merge($results, self::flatten($value, $prepend . $key . '.'));
|
||||
} else {
|
||||
$results[$prepend . $key] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
return $results;
|
||||
}
|
||||
}
|
||||
Executable
+351
@@ -0,0 +1,351 @@
|
||||
<?php
|
||||
|
||||
namespace Kernel;
|
||||
|
||||
use Twig\Environment;
|
||||
use Twig\Loader\FilesystemLoader;
|
||||
use Twig\TwigFunction;
|
||||
|
||||
class View
|
||||
{
|
||||
protected Environment $twig;
|
||||
|
||||
private static ?\Twig\Loader\LoaderInterface $loader = null;
|
||||
private static ?\Twig\Environment $environment = null;
|
||||
private static ?string $activeTheme = null;
|
||||
private static ?string $parentTheme = null;
|
||||
|
||||
public function __construct(?string $viewsPath = null, array $globals = [])
|
||||
{
|
||||
if (self::$environment) {
|
||||
$this->twig = self::$environment;
|
||||
} else {
|
||||
$this->initEngine($viewsPath);
|
||||
}
|
||||
|
||||
// Registers global variables
|
||||
foreach ($globals as $key => $value) {
|
||||
$this->twig->addGlobal($key, $value);
|
||||
}
|
||||
}
|
||||
|
||||
protected function initEngine(?string $viewsPath = null): void
|
||||
{
|
||||
$baseDir = dirname(__DIR__);
|
||||
$templatesDir = $baseDir . '/templates';
|
||||
$modulesDir = $baseDir . '/modules';
|
||||
$themesDir = $baseDir . '/themes';
|
||||
|
||||
// Load active theme from database if not already loaded
|
||||
if (self::$activeTheme === null) {
|
||||
try {
|
||||
$themeConfig = \Illuminate\Database\Capsule\Manager::table('system_config')
|
||||
->where('system_config_key', 'active_theme')
|
||||
->first();
|
||||
self::$activeTheme = $themeConfig ? $themeConfig->system_config_value : 'default';
|
||||
} catch (\Exception $e) {
|
||||
self::$activeTheme = 'default';
|
||||
}
|
||||
|
||||
// Check for parent theme in style.css
|
||||
$themePath = $themesDir . '/' . self::$activeTheme;
|
||||
$stylePath = $themePath . '/style.css';
|
||||
if (file_exists($stylePath)) {
|
||||
$content = file_get_contents($stylePath);
|
||||
if (preg_match('/Template:\s*(.*)$/mi', $content, $matches)) {
|
||||
self::$parentTheme = trim($matches[1]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$paths = [];
|
||||
|
||||
// 1. Priority: Active Theme
|
||||
$themePath = $themesDir . '/' . self::$activeTheme;
|
||||
if (is_dir($themePath)) {
|
||||
$paths[] = $themePath;
|
||||
if (is_dir($themePath . '/views')) {
|
||||
$paths[] = $themePath . '/views';
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Priority: Parent Theme (if exists)
|
||||
if (self::$parentTheme) {
|
||||
$parentPath = $themesDir . '/' . self::$parentTheme;
|
||||
if (is_dir($parentPath)) {
|
||||
$paths[] = $parentPath;
|
||||
if (is_dir($parentPath . '/views')) {
|
||||
$paths[] = $parentPath . '/views';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Global templates
|
||||
if (is_dir($templatesDir)) {
|
||||
$paths[] = $templatesDir;
|
||||
}
|
||||
|
||||
// 4. Modules
|
||||
if (is_dir($modulesDir)) {
|
||||
$paths[] = $modulesDir;
|
||||
}
|
||||
|
||||
// Custom views path if provided
|
||||
if ($viewsPath && is_dir($viewsPath)) {
|
||||
array_unshift($paths, $viewsPath);
|
||||
}
|
||||
|
||||
$loader = new FilesystemLoader($paths);
|
||||
|
||||
// Register namespaces for modules based on directory structure
|
||||
if (is_dir($modulesDir)) {
|
||||
$modules = array_diff(scandir($modulesDir), ['.', '..']);
|
||||
foreach ($modules as $module) {
|
||||
$moduleViewsPath = $modulesDir . '/' . $module . '/views';
|
||||
if (is_dir($moduleViewsPath)) {
|
||||
// Check for theme override for the entire module namespace
|
||||
$themeModulePath = $themesDir . '/' . self::$activeTheme . '/modules/' . $module . '/views';
|
||||
if (is_dir($themeModulePath)) {
|
||||
$loader->addPath($themeModulePath, $module);
|
||||
}
|
||||
|
||||
// Check for parent theme override
|
||||
if (self::$parentTheme) {
|
||||
$parentModulePath = $themesDir . '/' . self::$parentTheme . '/modules/' . $module . '/views';
|
||||
if (is_dir($parentModulePath)) {
|
||||
$loader->addPath($parentModulePath, $module);
|
||||
}
|
||||
}
|
||||
|
||||
// Add original module path as fallback
|
||||
$loader->addPath($moduleViewsPath, $module);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self::$loader = $loader;
|
||||
|
||||
$this->twig = new Environment($loader, [
|
||||
'cache' => $baseDir . '/cache/twig',
|
||||
'auto_reload' => true,
|
||||
'autoescape' => 'html',
|
||||
]);
|
||||
|
||||
self::$environment = $this->twig;
|
||||
|
||||
// Register default functions
|
||||
$this->registerDefaultFunctions();
|
||||
}
|
||||
|
||||
protected function registerDefaultFunctions(): void
|
||||
{
|
||||
// Load theme settings
|
||||
$themeSettings = [];
|
||||
if (self::$activeTheme) {
|
||||
try {
|
||||
$dbKey = 'theme_settings_' . self::$activeTheme;
|
||||
$config = \Illuminate\Database\Capsule\Manager::table('system_config')
|
||||
->where('system_config_key', $dbKey)
|
||||
->first();
|
||||
|
||||
if ($config) {
|
||||
$themeSettings = json_decode($config->system_config_value, true) ?: [];
|
||||
}
|
||||
|
||||
// If it's a child theme, we might want to merge with parent settings or defaults
|
||||
// For now, let's also load the customizer defaults if the key is missing
|
||||
$baseDir = dirname(__DIR__);
|
||||
$customizerPath = $baseDir . '/themes/' . self::$activeTheme . '/customizer.json';
|
||||
if (file_exists($customizerPath)) {
|
||||
$customizer = json_decode(file_get_contents($customizerPath), true);
|
||||
if (isset($customizer['sections'])) {
|
||||
foreach ($customizer['sections'] as $section) {
|
||||
foreach ($section['fields'] as $field) {
|
||||
if (!isset($themeSettings[$field['id']])) {
|
||||
$themeSettings[$field['id']] = $field['default'];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
// Ignore DB errors here
|
||||
}
|
||||
}
|
||||
$this->twig->addGlobal('theme_settings', $themeSettings);
|
||||
|
||||
// Platform name global variable
|
||||
try {
|
||||
$platformService = new \App\Services\PlatformService();
|
||||
$this->twig->addGlobal('platformName', $platformService->getPlatformName());
|
||||
} catch (\Exception $e) {
|
||||
$this->twig->addGlobal('platformName', 'SysCMS');
|
||||
}
|
||||
|
||||
// Translation function __() available in Twig views
|
||||
$this->twig->addFunction(new TwigFunction('__', function ($key, ...$params) {
|
||||
return __($key, ...$params);
|
||||
}));
|
||||
|
||||
$this->twig->addFunction(new TwigFunction('url', function ($path = '') {
|
||||
return url($path);
|
||||
}));
|
||||
|
||||
$this->twig->addFunction(new TwigFunction('asset_url', function ($path) {
|
||||
return asset_url($path);
|
||||
}));
|
||||
|
||||
$this->twig->addFunction(new TwigFunction('theme_url', function ($path) {
|
||||
$basePath = '/themes/' . (self::$activeTheme ?? 'default');
|
||||
$fullPath = dirname(__DIR__) . $basePath . '/' . ltrim($path, '/');
|
||||
|
||||
// If asset doesn't exist in child theme, use parent theme
|
||||
if (!file_exists($fullPath) && self::$parentTheme) {
|
||||
return '/themes/' . self::$parentTheme . '/' . ltrim($path, '/');
|
||||
}
|
||||
|
||||
return $basePath . '/' . ltrim($path, '/');
|
||||
}));
|
||||
|
||||
$this->twig->addFunction(new TwigFunction('theme_asset', function ($path) {
|
||||
$basePath = '/themes/' . (self::$activeTheme ?? 'default') . '/assets';
|
||||
$fullPath = dirname(__DIR__) . $basePath . '/' . ltrim($path, '/');
|
||||
|
||||
// If asset doesn't exist in child theme, use parent theme
|
||||
if (!file_exists($fullPath) && self::$parentTheme) {
|
||||
return '/themes/' . self::$parentTheme . '/assets/' . ltrim($path, '/');
|
||||
}
|
||||
|
||||
return $basePath . '/' . ltrim($path, '/');
|
||||
}));
|
||||
|
||||
$this->twig->addFunction(new TwigFunction('bus_module_enabled', function ($moduleName) {
|
||||
return bus_module_enabled($moduleName);
|
||||
}));
|
||||
|
||||
$this->twig->addFunction(new TwigFunction('get_config', function ($key, $default = null) {
|
||||
return get_config($key, $default);
|
||||
}));
|
||||
|
||||
$this->twig->addGlobal('active_theme', self::$activeTheme);
|
||||
$this->twig->addGlobal('parent_theme', self::$parentTheme);
|
||||
$this->twig->addGlobal('current_uri', $_SERVER['REQUEST_URI'] ?? '/');
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders a view with theme override support
|
||||
*/
|
||||
public function render(string $template, array $data = []): string
|
||||
{
|
||||
// Core Integrity Check (License Redundancy)
|
||||
if (!\Kernel\License::isValidDomain()) {
|
||||
throw new \RuntimeException('Core Integrity Error: Render Engine Initialization Failed (0x8F2).');
|
||||
}
|
||||
|
||||
// Check for theme overrides (Child Theme first, then Parent Theme)
|
||||
$overrideTemplate = $this->getThemeOverride($template);
|
||||
$templateToRender = $overrideTemplate ?: $template;
|
||||
|
||||
return $this->twig->render($templateToRender, $data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a theme override exists for a given template
|
||||
*/
|
||||
protected function getThemeOverride(string $template): ?string
|
||||
{
|
||||
if (self::$activeTheme === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$baseDir = dirname(__DIR__);
|
||||
$themes = [self::$activeTheme];
|
||||
if (self::$parentTheme) {
|
||||
$themes[] = self::$parentTheme;
|
||||
}
|
||||
|
||||
// Handle namespaced templates like @index/index.twig
|
||||
if (strpos($template, '@') === 0) {
|
||||
$parts = explode('/', ltrim($template, '@'), 2);
|
||||
if (count($parts) === 2) {
|
||||
$module = $parts[0];
|
||||
$view = $parts[1];
|
||||
|
||||
foreach ($themes as $theme) {
|
||||
$themePath = $baseDir . '/themes/' . $theme;
|
||||
|
||||
// Check in themes/{theme}/modules/{module}/views/{view}
|
||||
$checkPath = $themePath . '/modules/' . $module . '/views/' . $view;
|
||||
if (file_exists($checkPath)) {
|
||||
return 'modules/' . $module . '/views/' . $view;
|
||||
}
|
||||
|
||||
// Also check without 'views' subfolder
|
||||
$checkPathAlt = $themePath . '/modules/' . $module . '/' . $view;
|
||||
if (file_exists($checkPathAlt)) {
|
||||
return 'modules/' . $module . '/' . $view;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Directly displays the view
|
||||
*/
|
||||
public function display(string $template, array $data = []): void
|
||||
{
|
||||
echo $this->render($template, $data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the Twig instance
|
||||
*/
|
||||
public function getEngine(): Environment
|
||||
{
|
||||
return $this->twig;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the Twig loader
|
||||
*/
|
||||
public static function setLoader(\Twig\Loader\LoaderInterface $loader): void
|
||||
{
|
||||
self::$loader = $loader;
|
||||
}
|
||||
|
||||
public static function getLoader(): ?\Twig\Loader\LoaderInterface
|
||||
{
|
||||
return self::$loader;
|
||||
}
|
||||
|
||||
public static function getActiveTheme(): ?string
|
||||
{
|
||||
return self::$activeTheme;
|
||||
}
|
||||
|
||||
public static function getParentTheme(): ?string
|
||||
{
|
||||
return self::$parentTheme;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the Twig environment
|
||||
*/
|
||||
public static function setEnvironment(\Twig\Environment $environment): void
|
||||
{
|
||||
self::$environment = $environment;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a path for module views
|
||||
*/
|
||||
public static function addPath(string $namespace, string $path): void
|
||||
{
|
||||
if (self::$loader instanceof \Twig\Loader\FilesystemLoader) {
|
||||
self::$loader->addPath($path, $namespace);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user