diff --git a/classes/core/blade/BladeCompiler.php b/classes/core/blade/BladeCompiler.php index 7773b597b33..af10b014f23 100644 --- a/classes/core/blade/BladeCompiler.php +++ b/classes/core/blade/BladeCompiler.php @@ -9,13 +9,13 @@ * * @class BladeCompiler * - * @brief This overrides the default BladeCompiler to use the overridden ComponentTagCompiler + * @brief Extended BladeCompiler that supports Smarty template includes. */ namespace PKP\core\blade; -use PKP\core\blade\ComponentTagCompiler; use Illuminate\View\Compilers\BladeCompiler as IlluminateBladeCompiler; +use PKP\template\PKPTemplateResource; class BladeCompiler extends IlluminateBladeCompiler { @@ -26,7 +26,7 @@ class BladeCompiler extends IlluminateBladeCompiler */ protected function compileComponentTags($value) { - if (! $this->compilesComponentTags) { + if (!$this->compilesComponentTags) { return $value; } @@ -38,4 +38,61 @@ protected function compileComponentTags($value) ) )->compile($value); } + + /** + * Override compileInclude to support Smarty template includes from Blade. + * + * Uses PKPTemplateResource::getFilePath() to resolve the template. + * If it resolves to a .tpl file, generates code to render via Smarty. + * If it resolves to a .blade file, uses View::file() for direct rendering. + * + * @see \Illuminate\View\Compilers\Concerns\CompilesIncludes::compileInclude() + * + * @param string $expression The @include expression + * + * @return string Compiled PHP code + */ + protected function compileInclude($expression) + { + $expression = $this->stripParentheses($expression); + + // Extract the view name from the expression + $viewName = $this->extractViewName($expression); + + if ($viewName) { + $filePath = PKPTemplateResource::getFilePath($viewName); + + if ($filePath) { + // If it's a Smarty template, render via TemplateManager + if (!PKPTemplateResource::isBladeTemplate($filePath)) { + $normalizedName = PKPTemplateResource::normalizeTemplateName($viewName); + return "getRequest())->fetch('{$normalizedName}.tpl'); ?>"; + } + + // For Blade templates, use View::file() with the resolved path + $escapedPath = addslashes($filePath); + return "render(); ?>"; + } + } + + // Default behavior: use Laravel's view system + return parent::compileInclude($expression); + } + + /** + * Extract the view name from the @include expression. + * + * @param string $expression The expression (e.g., "'frontend.objects.article_details'" or "'view', ['data']") + * + * @return string|null The view name without quotes, or null if cannot extract + */ + protected function extractViewName(string $expression): ?string + { + // Match single or double quoted string at the start + if (preg_match('/^["\']([^"\']+)["\']/', $expression, $matches)) { + return $matches[1]; + } + + return null; + } } diff --git a/classes/plugins/Plugin.php b/classes/plugins/Plugin.php index c3ca57f0a4c..2f548148320 100644 --- a/classes/plugins/Plugin.php +++ b/classes/plugins/Plugin.php @@ -50,7 +50,6 @@ use APP\core\Application; use APP\template\TemplateManager; use Exception; -use Illuminate\Database\Migrations\Migration; use PKP\config\Config; use PKP\core\JSONMessage; use PKP\core\PKPApplication; @@ -233,7 +232,7 @@ final public function getInstallSchemaFile() /** * Get the installation migration for this plugin. * - * @return ?Migration + * @return \Illuminate\Database\Migrations\Migration|\PKP\migration\Migration|null */ public function getInstallMigration() { @@ -431,15 +430,13 @@ public function getComponentClassNamespace(): string */ public function resolveBladeViewPath(string $templatePath): string { - // This is to accommodate the case if template files path beign set as similar - // to the smarty templates e.g. `some-path/some-template.blade.php` - // as blade view needed to in just `some-path.some-template` + // Convert template path to Blade view notation + // e.g. `some-path/some-template.blade` → `some-path.some-template` $bladeTemplatePath = str_replace( - ['/', '.blade.php', '.blade', '.tpl'], - ['.', '', '', ''], + ['/', '.blade', '.tpl'], + ['.', '', ''], $templatePath ); - return "{$this->getTemplateViewNamespace()}::{$bladeTemplatePath}"; } @@ -514,68 +511,47 @@ protected function _registerViewComponentNamespace(string $componentNamespacePat public function _overridePluginTemplates($hookName, $args) { $filePath = &$args[0]; - $template = $args[1]; + + // Get template paths to search (themes override this to include parent themes) + $templatePaths = $this->getTemplatePaths(); + if (empty($templatePaths)) { + return false; + } + $checkFilePath = $filePath; - // If there's a templates/ prefix on the template, clean up the test path. - if (strpos($filePath, 'plugins/') === 0) { - $checkFilePath = 'templates/' . $checkFilePath; + // If there's a lib/pkp/ prefix, strip it + if (str_starts_with($checkFilePath, 'lib/pkp/')) { + $checkFilePath = substr($checkFilePath, strlen('lib/pkp/')); } - // If there's a lib/pkp/ prefix on the template, test without it. - $libPkpPrefix = 'lib/pkp/'; - if (strpos($checkFilePath, $libPkpPrefix) === 0) { - $checkFilePath = substr($filePath, strlen($libPkpPrefix)); + // If path starts with templates/, strip it since getTemplatePaths() + // already returns paths ending with /templates + if (str_starts_with($checkFilePath, 'templates/')) { + $checkFilePath = substr($checkFilePath, strlen('templates/')); } - // Check if an overriding plugin exists in the plugin path. - if ($overriddenFilePath = $this->_findOverriddenTemplate($checkFilePath)) { - $filePath = $overriddenFilePath; + // Normalize and search for override + $baseName = preg_replace('/\.(tpl|blade)$/', '', $checkFilePath); + $override = \PKP\template\PKPTemplateResource::findInPaths($baseName, $templatePaths); + + if ($override) { + $filePath = $override; } return false; } /** - * Recursive check for existing templates - * - * @param string $path + * Get template paths for this plugin to search for overrides. + * Themes override this to include parent theme paths. * - * @return string|null + * @return array Array of template directory paths */ - private function _findOverriddenTemplate($path) + protected function getTemplatePaths(): array { - $fullPath = sprintf('%s/%s', $this->getPluginPath(), $path); - - // If smarty template exists, return the full path - if (file_exists($fullPath)) { - return $fullPath; - } - - // Fallback to blade view if exists for theme plugins and if the parent template is a blade view - if ($this instanceof \PKP\plugins\ThemePlugin && $this->isRenderingViaBladeView) { - $bladePath = $this->resolveBladeViewPath(str_replace('templates/', '', $path)); - if (view()->exists($bladePath)) { - return $bladePath; - } - } - - // Backward compatibility for OJS prior to 3.1.2; changed path to templates for plugins. - if (($fullPath = preg_replace("/templates\/(?!.*templates\/)/", '', $fullPath)) && file_exists($fullPath)) { - if (Config::getVar('debug', 'deprecation_warnings')) { - trigger_error('Deprecated: The template at ' . $fullPath . ' has moved and will not be found in the future.'); - } - return $fullPath; - } - - // Recursive check for templates in ancestors of a current theme plugin - if ($this instanceof ThemePlugin - && $this->parent - && $fullPath = $this->parent->_findOverriddenTemplate($path)) { - return $fullPath; - } - - return null; + $path = $this->getPluginPath() . '/templates'; + return is_dir($path) ? [$path] : []; } /** diff --git a/classes/plugins/ThemePlugin.php b/classes/plugins/ThemePlugin.php index 70ddb98fc5f..8d2cd1fad18 100644 --- a/classes/plugins/ThemePlugin.php +++ b/classes/plugins/ThemePlugin.php @@ -94,11 +94,6 @@ abstract class ThemePlugin extends LazyLoadPlugin */ public bool $isVueRuntimeRequired = false; - /** - * Track whether rendering via blade view - */ - public bool $isRenderingViaBladeView = false; - /** * @copydoc Plugin::register * @@ -122,30 +117,11 @@ public function register($category, $path, $mainContextId = null) Hook::add('PluginRegistry::categoryLoaded::themes', $this->initAfter(...)); // Allow themes to override plugin template files - Hook::add('TemplateManager::display', $this->loadBladeView(...)); Hook::add('TemplateResource::getFilename', $this->_overridePluginTemplates(...)); return true; } - /** - * Register the blade view path by replacing the smarty template path in the TemplateManager - * only if the blade view exists - */ - public function loadBladeView(string $hookName, array $params): bool - { - $templateManager =& $params[0]; /** @var TemplateManager $templateManager */ - $templatePath =& $params[1]; /** @var string $templatePath */ - - $bladeViewPath = $this->resolveBladeViewPath($templatePath); - if (view()->exists($bladeViewPath)) { - $this->isRenderingViaBladeView = true; - $templatePath = $bladeViewPath; - } - - return Hook::CONTINUE; - } - /** * Fire the init() method when a theme is registered * @@ -784,19 +760,32 @@ public function setParent($parent) /** * Register directories to search for template files - * */ private function _registerTemplates() { - // Register parent theme template directory - if (isset($this->parent) && $this->parent instanceof self) { - $this->parent->_registerTemplates(); + // Template paths are now computed on demand in getTemplatePaths() + } + + /** + * Get template paths for this theme including parent themes. + * Overrides Plugin::getTemplatePaths() to include theme hierarchy. + * + * @return array Array of template directory paths (child to parent order) + */ + protected function getTemplatePaths(): array + { + $paths = []; + $theme = $this; + + while ($theme) { + $themePath = $theme->getPluginPath() . '/templates'; + if (is_dir($themePath)) { + $paths[] = $themePath; + } + $theme = $theme->parent ?? null; } - // Register this theme's template directory - $request = Application::get()->getRequest(); - $templateManager = TemplateManager::getManager($request); - $templateManager->addTemplateDir($this->_getBaseDir('templates')); + return $paths; } /** @@ -1065,13 +1054,11 @@ protected function getUsageStatsDisplayColor(int $num): string */ protected function getSubmissionViewContext(): string { - if (Application::get()->getName() == 'ojs2') { - return 'frontend-article-view'; - } elseif (Application::get()->getName() == 'omp') { - return 'frontend-catalog-book'; - } elseif (Application::get()->getName() == 'ops') { - return 'frontend-preprint-view'; - } + return match (Application::get()->getName()) { + 'ojs2' => 'frontend-article-view', + 'omp' => 'frontend-catalog-book', + 'ops' => 'frontend-preprint-view', + }; } /** diff --git a/classes/template/PKPTemplateManager.php b/classes/template/PKPTemplateManager.php index 79c9f6401ad..c15cef985bf 100644 --- a/classes/template/PKPTemplateManager.php +++ b/classes/template/PKPTemplateManager.php @@ -243,9 +243,22 @@ public function initialize(PKPRequest $request) $activeTheme = null; $contextOrSite = $currentContext ? $currentContext : $request->getSite(); $allThemes = PluginRegistry::getPlugins('themes'); - foreach ($allThemes as $theme) { + foreach ($allThemes as $theme) { /** @var \PKP\plugins\Plugin|\PKP\plugins\ThemePlugin $theme */ if ($contextOrSite->getData('themePluginPath') === $theme->getDirName()) { $activeTheme = $theme; + + $bladeFileViewFinder = app()->get('view.finder'); /** @var \Illuminate\View\FileViewFinder $bladeFileViewFinder */ + // Along with current active theme, + // we should also register the path of any parent theme + while($theme) { + if ($theme->getTemplatePath()) { + $bladeFileViewFinder->prependLocation( + app()->basePath($theme->getTemplatePath()) + ); + } + $theme = $theme->parent; + } + break; } } diff --git a/classes/template/PKPTemplateResource.php b/classes/template/PKPTemplateResource.php index e0af98f6b7b..82106a41542 100644 --- a/classes/template/PKPTemplateResource.php +++ b/classes/template/PKPTemplateResource.php @@ -16,13 +16,20 @@ namespace PKP\template; +use APP\core\Application; +use APP\template\TemplateManager; +use Illuminate\Support\Facades\View; use PKP\plugins\Hook; +use Throwable; class PKPTemplateResource extends \Smarty_Resource_Custom { - /** @var array|string Template path or list of paths */ + /** @var array Template path or list of paths */ protected $_templateDir; + /** @var array Resolution cache */ + protected static array $cache = []; + /** * Constructor * @@ -48,13 +55,31 @@ public function __construct($templateDir) */ public function fetch($name, &$source, &$mtime) { - $filename = $this->_getFilename($name); - $mtime = filemtime($filename); + $filePath = $this->_getFilename($name); + + if (!$filePath || !file_exists($filePath)) { + return false; + } + + $mtime = filemtime($filePath); if ($mtime === false) { return false; } - $source = file_get_contents($filename); + // Blade template - render via View::file() + if (str_ends_with($filePath, '.blade')) { + try { + $templateManager = TemplateManager::getManager(Application::get()->getRequest()); + $source = View::file($filePath, $templateManager->getTemplateVars())->render(); + return true; + } catch (Throwable $e) { + error_log("Error rendering Blade template '{$filePath}': " . $e->getMessage()); + throw $e; + } + } + + // Smarty template - return file contents + $source = file_get_contents($filePath); return ($source !== false); } @@ -67,7 +92,13 @@ public function fetch($name, &$source, &$mtime) */ protected function fetchTimestamp($name) { - return filemtime($this->_getFilename($name)); + $filePath = $this->_getFilename($name); + + if (!$filePath) { + return false; + } + + return filemtime($filePath); } /** @@ -79,14 +110,105 @@ protected function fetchTimestamp($name) */ protected function _getFilename($template) { - $filePath = null; - foreach ($this->_templateDir as $path) { - $filePath = "{$path}/{$template}"; - if (file_exists($filePath)) { - break; - } + if (array_key_exists($template, self::$cache)) { + return self::$cache[$template] ?: null; } + + $baseName = self::normalizeTemplateName($template); + $filePath = self::findInPaths($baseName, $this->_templateDir); + Hook::call('TemplateResource::getFilename', [&$filePath, $template]); + + self::$cache[$template] = $filePath ?: false; return $filePath; } + + /** + * Find a template in the given paths, checking .blade first then .tpl. + * + * @param string $baseName Normalized template name without extension + * @param array $paths Array of directory paths to search + * + * @return string|null The file path if found, null otherwise + */ + public static function findInPaths(string $baseName, array $paths): ?string + { + foreach ($paths as $path) { + $bladePath = "{$path}/{$baseName}.blade"; + if (file_exists($bladePath)) { + return $bladePath; + } + + $smartyPath = "{$path}/{$baseName}.tpl"; + if (file_exists($smartyPath)) { + return $smartyPath; + } + } + + return null; + } + + /** + * Normalize template name to base path without extension. + * + * @param string $name Template name in various formats + * + * @return string Normalized base name (e.g., 'frontend/pages/article') + */ + public static function normalizeTemplateName(string $name): string + { + $name = preg_replace('/\.(tpl|blade)$/', '', $name); + + if (!str_contains($name, '/') && str_contains($name, '.')) { + $name = str_replace('.', '/', $name); + } + + $name = preg_replace('#^templates/#', '', $name); + + return $name; + } + + /** + * Static method to get a template file path (for use by other classes). + * + * @param string $template Template name + * + * @return string|null The file path, or null if not found + */ + public static function getFilePath(string $template): ?string + { + if (array_key_exists($template, self::$cache)) { + return self::$cache[$template] ?: null; + } + + $templateManager = TemplateManager::getManager(Application::get()->getRequest()); + $resource = $templateManager->registered_resources['app'] ?? null; + + if ($resource instanceof self) { + return $resource->_getFilename($template); + } + + $instance = new self(['templates', 'lib/pkp/templates']); + return $instance->_getFilename($template); + } + + /** + * Check if a file path is a Blade template. + * + * @param string $filePath The file path to check + * + * @return bool + */ + public static function isBladeTemplate(string $filePath): bool + { + return str_ends_with($filePath, '.blade'); + } + + /** + * Clear the resolution cache. + */ + public static function clearCache(): void + { + self::$cache = []; + } }