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); } } }