Theme: Changed how before/after views are registered

Changed the system out to be a theme event instead of method, to align
with other registration events, and so that the theme view work can
better be contained in its own class.
This commit is contained in:
Dan Brown
2026-01-27 16:50:50 +00:00
parent 9fcfc762ec
commit 1b17bb3929
5 changed files with 135 additions and 100 deletions

View File

@@ -4,9 +4,9 @@ namespace BookStack\App\Providers;
use BookStack\Theming\ThemeEvents;
use BookStack\Theming\ThemeService;
use BookStack\Theming\ThemeViews;
use Illuminate\Support\Facades\Blade;
use Illuminate\Support\ServiceProvider;
use Illuminate\View\View;
class ThemeServiceProvider extends ServiceProvider
{
@@ -26,16 +26,21 @@ class ThemeServiceProvider extends ServiceProvider
{
// Boot up the theme system
$themeService = $this->app->make(ThemeService::class);
$viewFactory = $this->app->make('view');
$themeService->registerViewPathsForTheme($viewFactory->getFinder());
if (!$themeService->getTheme()) {
return;
}
if ($themeService->logicalThemeIsActive()) {
$themeService->readThemeActions();
$themeService->dispatch(ThemeEvents::APP_BOOT, $this->app);
$viewFactory->share('__theme', $themeService);
$themeService->readThemeActions();
$themeService->dispatch(ThemeEvents::APP_BOOT, $this->app);
$themeViews = new ThemeViews();
$themeService->dispatch(ThemeEvents::THEME_REGISTER_VIEWS, $themeViews);
$themeViews->registerViewPathsForTheme($viewFactory->getFinder());
if ($themeViews->hasRegisteredViews()) {
$viewFactory->share('__themeViews', $themeViews);
Blade::directive('include', function ($expression) {
return "<?php echo \$__theme->handleViewInclude({$expression}, array_diff_key(get_defined_vars(), ['__data' => 1, '__path' => 1])); ?>";
return "<?php echo \$__themeViews->handleViewInclude({$expression}, array_diff_key(get_defined_vars(), ['__data' => 1, '__path' => 1])); ?>";
});
}
}

View File

@@ -134,6 +134,16 @@ class ThemeEvents
*/
const ROUTES_REGISTER_WEB_AUTH = 'routes_register_web_auth';
/**
* Theme register views event.
* Called by the theme system when a theme is active, so that custom view templates can be registered
* to be rendered in addition to existing app views.
*
* @param \BookStack\Theming\ThemeViews $themeViews
*/
const THEME_REGISTER_VIEWS = 'theme_register_views';
/**
* Web before middleware action.
* Runs before the request is handled but after all other middleware apart from those

View File

@@ -16,16 +16,6 @@ class ThemeService
*/
protected array $listeners = [];
/**
* @var array<string, array<string, int>>
*/
protected array $beforeViews = [];
/**
* @var array<string, array<string, int>>
*/
protected array $afterViews = [];
/**
* Get the currently configured theme.
* Returns an empty string if not configured.
@@ -92,6 +82,10 @@ class ThemeService
public function readThemeActions(): void
{
$themeActionsFile = theme_path('functions.php');
if (!$themeActionsFile || !file_exists($themeActionsFile)) {
return;
}
try {
require $themeActionsFile;
} catch (\Error $exception) {
@@ -99,24 +93,6 @@ class ThemeService
}
}
/**
* Check if a logical theme is active.
*/
public function logicalThemeIsActive(): bool
{
$themeActionsFile = theme_path('functions.php');
return $themeActionsFile && file_exists($themeActionsFile);
}
/**
* Register any extra paths for where we may expect views to be located
* with the provided FileViewFinder, to make custom views available for use.
*/
public function registerViewPathsForTheme(FileViewFinder $finder): void
{
$finder->prependLocation(theme_path());
}
/**
* @see SocialDriverManager::addSocialDriver
*/
@@ -125,63 +101,4 @@ class ThemeService
$driverManager = app()->make(SocialDriverManager::class);
$driverManager->addSocialDriver($driverName, $config, $socialiteHandler, $configureForRedirect);
}
/**
* Provide the response for a blade template view include.
*/
public function handleViewInclude(string $viewPath, array $data = []): string
{
$viewsContent = [
...$this->renderViewSets($this->beforeViews[$viewPath] ?? [], $data),
view()->make($viewPath, $data)->render(),
...$this->renderViewSets($this->afterViews[$viewPath] ?? [], $data),
];
return implode("\n", $viewsContent);
}
/**
* Register a custom view to be rendered before the given target view is included in the template system.
*/
public function registerViewToRenderBefore(string $targetView, string $localView, int $priority = 50): void
{
$this->registerAdjacentView($this->beforeViews, $targetView, $localView, $priority);
}
/**
* Register a custom view to be rendered after the given target view is included in the template system.
*/
public function registerViewToRenderAfter(string $targetView, string $localView, int $priority = 50): void
{
$this->registerAdjacentView($this->afterViews, $targetView, $localView, $priority);
}
protected function registerAdjacentView(array &$location, string $targetView, string $localView, int $priority = 50): void
{
$viewPath = theme_path($localView . '.blade.php');
if (!file_exists($viewPath)) {
throw new ThemeException("Expected registered view file at \"{$viewPath}\" does not exist");
}
if (!isset($location[$targetView])) {
$location[$targetView] = [];
}
$location[$targetView][$viewPath] = $priority;
}
/**
* @param array<string, int> $viewSet
* @return string[]
*/
protected function renderViewSets(array $viewSet, array $data): array
{
$paths = array_keys($viewSet);
usort($paths, function (string $a, string $b) use ($viewSet) {
return $viewSet[$a] <=> $viewSet[$b];
});
return array_map(function (string $viewPath) use ($data) {
return view()->file($viewPath, $data)->render();
}, $paths);
}
}

View File

@@ -0,0 +1,96 @@
<?php
namespace BookStack\Theming;
use BookStack\Exceptions\ThemeException;
use Illuminate\View\FileViewFinder;
class ThemeViews
{
/**
* @var array<string, array<string, int>>
*/
protected array $beforeViews = [];
/**
* @var array<string, array<string, int>>
*/
protected array $afterViews = [];
/**
* Register any extra paths for where we may expect views to be located
* with the provided FileViewFinder, to make custom views available for use.
*/
public function registerViewPathsForTheme(FileViewFinder $finder): void
{
$finder->prependLocation(theme_path());
}
/**
* Provide the response for a blade template view include.
*/
public function handleViewInclude(string $viewPath, array $data = []): string
{
if (!$this->hasRegisteredViews()) {
return view()->make($viewPath, $data)->render();
}
$viewsContent = [
...$this->renderViewSets($this->beforeViews[$viewPath] ?? [], $data),
view()->make($viewPath, $data)->render(),
...$this->renderViewSets($this->afterViews[$viewPath] ?? [], $data),
];
return implode("\n", $viewsContent);
}
/**
* Register a custom view to be rendered before the given target view is included in the template system.
*/
public function renderBefore(string $targetView, string $localView, int $priority = 50): void
{
$this->registerAdjacentView($this->beforeViews, $targetView, $localView, $priority);
}
/**
* Register a custom view to be rendered after the given target view is included in the template system.
*/
public function renderAfter(string $targetView, string $localView, int $priority = 50): void
{
$this->registerAdjacentView($this->afterViews, $targetView, $localView, $priority);
}
public function hasRegisteredViews(): bool
{
return !empty($this->beforeViews) && !empty($this->afterViews);
}
protected function registerAdjacentView(array &$location, string $targetView, string $localView, int $priority = 50): void
{
$viewPath = theme_path($localView . '.blade.php');
if (!file_exists($viewPath)) {
throw new ThemeException("Expected registered view file at \"{$viewPath}\" does not exist");
}
if (!isset($location[$targetView])) {
$location[$targetView] = [];
}
$location[$targetView][$viewPath] = $priority;
}
/**
* @param array<string, int> $viewSet
* @return string[]
*/
protected function renderViewSets(array $viewSet, array $data): array
{
$paths = array_keys($viewSet);
usort($paths, function (string $a, string $b) use ($viewSet) {
return $viewSet[$a] <=> $viewSet[$b];
});
return array_map(function (string $viewPath) use ($data) {
return view()->file($viewPath, $data)->render();
}, $paths);
}
}

View File

@@ -492,7 +492,7 @@ END;
});
}
public function test_register_view_to_render_before_and_after()
public function test_theme_register_views_event_to_insert_views_before_and_after()
{
$this->usingThemeFolder(function (string $folder) {
$before = 'this-is-my-before-header-string';
@@ -502,10 +502,14 @@ END;
$functionsContent = <<<'CONTENT'
<?php use BookStack\Facades\Theme;
Theme::registerViewToRenderBefore('layouts.parts.header', 'before', 4);
Theme::registerViewToRenderAfter('layouts.parts.header', 'after-a', 4);
Theme::registerViewToRenderAfter('layouts.parts.header', 'after-b', 1);
Theme::registerViewToRenderAfter('layouts.parts.header', 'after-c', 12);
use BookStack\Theming\ThemeEvents;
use BookStack\Theming\ThemeViews;
Theme::listen(ThemeEvents::THEME_REGISTER_VIEWS, function (ThemeViews $themeViews) {
$themeViews->renderBefore('layouts.parts.header', 'before', 4);
$themeViews->renderAfter('layouts.parts.header', 'after-a', 4);
$themeViews->renderAfter('layouts.parts.header', 'after-b', 1);
$themeViews->renderAfter('layouts.parts.header', 'after-c', 12);
});
CONTENT;
$viewDir = theme_path();
@@ -516,12 +520,15 @@ CONTENT;
file_put_contents($viewDir . '/after-c.blade.php', $afterC);
$this->refreshApplication();
$this->artisan('view:clear');
$resp = $this->get('/login');
$resp->assertSee($before);
// Ensure ordering of the multiple after views
$resp->assertSee($afterB . "\n" . $afterA . "\nthis-is-my-after-header-string-52");
});
$this->artisan('view:clear');
}
protected function usingThemeFolder(callable $callback)