Files
Helpddesk/Modules/Stylist/View/ThemeViewResolver.php
2026-09-02 17:08:57 +00:00

140 lines
4.2 KiB
PHP
Executable File

<?php
namespace Modules\Stylist\View;
use Illuminate\Contracts\View\Factory as ViewFactory;
use Illuminate\Contracts\View\View;
use Modules\Stylist\Theme\Exceptions\ThemeNotFoundException;
use Modules\Stylist\Theme\Loader;
use Modules\Stylist\Theme\Stylist;
use RuntimeException;
final class ThemeViewResolver
{
/** @var array<string, array<int, string>> */
private array $candidateCache = [];
public function __construct(
private readonly Stylist $stylist,
private readonly ViewFactory $views,
private readonly Loader $loader,
) {}
public function make(string $view, array $data = [], array $mergeData = []): View
{
return $this->views->first(
$this->candidates($view),
array_replace([
'themeSettings' => $this->stylist->settings(),
'themeVariant' => $this->stylist->variant(),
], $data),
$mergeData,
);
}
public function makeForTheme(string $theme, string $view, array $data = [], array $mergeData = []): View
{
return $this->views->first($this->candidatesForTheme($theme, $view), $data, $mergeData);
}
/** @return array<int, string> */
public function candidates(string $view): array
{
return $this->candidatesForTheme($this->stylist->current()->key(), $view);
}
/** @return array<int, string> */
/** @return array<int, string> */
public function themedCandidatesForTheme(string $theme, string $view): array
{
return array_values(array_filter(
$this->candidatesForTheme($theme, $view),
fn (string $candidate): bool => str_contains($candidate, '::'),
));
}
public function candidatesForTheme(string $theme, string $view): array
{
$view = ltrim($view, '.');
$cacheKey = $theme.'|'.$view;
if (isset($this->candidateCache[$cacheKey])) {
return $this->candidateCache[$cacheKey];
}
$keys = $this->inheritanceChain($theme);
$fallback = (string) config('stylist.default_theme', 'common');
if ($fallback !== '' && ! in_array($fallback, $keys, true)) {
try {
$keys = array_merge($keys, $this->inheritanceChain($fallback));
} catch (ThemeNotFoundException) {
// A missing configured fallback is diagnosed by stylist:doctor.
}
}
if (! in_array('common', $keys, true)) {
try {
$keys = array_merge($keys, $this->inheritanceChain('common'));
} catch (ThemeNotFoundException) {
// The module fallback below remains available.
}
}
$candidates = [];
foreach (array_values(array_unique($keys)) as $key) {
$this->registerNamespace($key);
$candidates[] = "stylist-theme-{$key}::{$view}";
$candidates[] = "stylist-module-theme-{$key}::{$view}";
}
$candidates[] = "stylist::{$view}";
$candidates[] = $view;
return $this->candidateCache[$cacheKey] = array_values(array_unique($candidates));
}
public function flush(): void
{
$this->candidateCache = [];
}
/** @return array<int, string> */
private function inheritanceChain(string $theme): array
{
$keys = [];
$seen = [];
$key = $theme;
while ($key !== '') {
if (isset($seen[$key])) {
throw new RuntimeException('Theme inheritance cycle detected at ['.$key.'].');
}
$seen[$key] = true;
$loaded = $this->loader->find($key);
$keys[] = $loaded->key();
$key = $loaded->parent() ?? '';
}
return $keys;
}
private function registerNamespace(string $theme): void
{
try {
$loaded = $this->loader->find($theme);
} catch (ThemeNotFoundException) {
return;
}
$viewsPath = $loaded->path().DIRECTORY_SEPARATOR.'views';
if (! is_dir($viewsPath)) {
return;
}
$this->views->addNamespace("stylist-theme-{$theme}", $viewsPath);
$this->views->addNamespace("stylist-module-theme-{$theme}", $viewsPath);
}
}