352 lines
12 KiB
PHP
Executable File
352 lines
12 KiB
PHP
Executable File
<?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);
|
|
}
|
|
}
|
|
}
|