First Commit

This commit is contained in:
marco
2026-09-01 10:46:29 -03:00
commit bf96b77e9c
10000 changed files with 3508600 additions and 0 deletions
+414
View File
@@ -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();
}
}
+47
View File
@@ -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;
}
}
+62
View File
@@ -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;
}
}
+201
View File
@@ -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;
}
}
+166
View File
@@ -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;
}
}
+351
View File
@@ -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);
}
}
}