\s*'[^']*'/", "'active_theme' => '" . addslashes($themeName) . "'", $config ); if ($newConfig !== $config) { if (file_put_contents($configFile, $newConfig) === false) { return false; } } self::$currentTheme = $themeName; return true; } return false; } /** * Check if a theme exists * * @param string $themeName * @return bool */ public static function themeExists(string $themeName): bool { // Default theme always exists as it uses core templates if ($themeName === 'default') { return true; } $themePath = self::getThemePath($themeName); return is_dir($themePath) && file_exists("$themePath/config.php"); } /** * Get the path to a theme * * @param string|null $themeName * @return string */ public static function getThemePath(?string $themeName = null): string { $themeName = $themeName ?? self::getCurrentThemeName(); $config = self::getConfig(); return rtrim($config['paths']['themes'], '/') . "/$themeName"; } /** * Get the URL for a theme asset * * @param string $path * @param bool $includeVersion * @return string */ public static function asset($path, $includeVersion = false) { $themeName = self::getCurrentThemeName(); $config = self::getConfig(); $baseUrl = rtrim($GLOBALS['app_root'] ?? '', '/'); // For non-default themes, use theme assets if ($themeName !== 'default') { $assetPath = "/themes/{$themeName}/assets/" . ltrim($path, '/'); // Add version query string for cache busting if ($includeVersion) { $version = self::getThemeVersion($themeName); $assetPath .= (strpos($assetPath, '?') !== false ? '&' : '?') . 'v=' . $version; } } else { // For default theme, use public_html directly $assetPath = '/' . ltrim($path, '/'); } return $baseUrl . $assetPath; } /** * Include a theme template file * * @param string $template Template name without .php extension * @return void */ public static function include($template) { global $config; $config = $config ?? []; $themeConfig = self::getConfig(); $themeName = self::getCurrentThemeName(); // We need this here, otherwise because this helper // between index and the views breaks the session vars extract($GLOBALS, EXTR_SKIP | EXTR_REFS); // Ensure config is always available in templates $config = array_merge($config, $themeConfig); // For non-default themes, look in the theme directory first if ($themeName !== 'default') { $themePath = $config['paths']['themes'] . '/' . $themeName . '/views/' . $template . '.php'; if (file_exists($themePath)) { include $themePath; return; } } // Fallback to default template location $defaultPath = $config['paths']['templates'] . '/' . $template . '.php'; if (file_exists($defaultPath)) { include $defaultPath; return; } // Log error if template not found error_log("Template not found: {$template} in theme: {$themeName}"); } /** * Get all available themes * * @return array */ public static function getAvailableThemes(): array { $config = self::getConfig(); $availableThemes = $config['available_themes'] ?? []; $themes = []; // Add default theme if not already present if (!isset($availableThemes['default'])) { $availableThemes['default'] = 'Default built-in theme'; } // Verify each theme exists and has a config file $themesDir = $config['paths']['themes'] ?? (__DIR__ . '/../../themes'); foreach ($availableThemes as $id => $name) { if ($id === 'default' || (is_dir("$themesDir/$id") && file_exists("$themesDir/$id/config.php"))) { $themes[$id] = $name; } } return $themes; } } // Initialize the theme system Theme::init();