Files
larablog/app/Domain/Theme/ThemeManager.php
T
ak 263b98b218 Initial baseline: LaraBlog core with plugin commerce surface.
Captures the current working tree after theme slots, ArticleAccess, and the payment / paid-content plugins so subsequent work has a reviewable git history.
2026-08-12 01:15:38 +08:00

193 lines
5.1 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Domain\Theme;
use App\Settings\GeneralSettings;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Facades\View;
use InvalidArgumentException;
use RuntimeException;
use Throwable;
class ThemeManager
{
protected string $activeTheme = 'default';
/** @var array<string, array<string, mixed>> */
protected array $discovered = [];
public function discover(): Collection
{
$themePath = config('larablog.theme_path', base_path('themes'));
if (! is_dir($themePath)) {
return collect();
}
$this->discovered = collect(File::directories($themePath))
->mapWithKeys(function (string $directory) use ($themePath): array {
$slug = basename($directory);
$manifestPath = $directory.'/theme.json';
$manifest = [];
if (is_file($manifestPath)) {
$decoded = json_decode((string) file_get_contents($manifestPath), true);
$manifest = is_array($decoded) ? $decoded : [];
}
$theme = array_merge([
'slug' => $slug,
'name' => $slug,
'path' => $directory,
], $manifest);
$fallback = $slug === 'default' ? null : $themePath.'/default/views';
$theme['slot_report'] = ThemeSlotReport::analyze($slug, $manifest, $directory, $fallback);
return [$slug => $theme];
})
->all();
return collect($this->discovered);
}
/**
* Slots declared in theme.json (expanded). Empty if undeclared.
*
* @return list<string>
*/
public function declaredSlots(?string $slug = null): array
{
$slug ??= $this->active();
$theme = $this->discover()->get($slug);
if (! is_array($theme)) {
return [];
}
return $theme['slot_report']['declared'] ?? [];
}
/**
* @return array<string, mixed>
*/
public function slotReport(?string $slug = null): array
{
$slug ??= $this->active();
$theme = $this->discover()->get($slug);
if (! is_array($theme) || ! isset($theme['slot_report'])) {
return ThemeSlotReport::analyze($slug, [], $this->path($slug));
}
return $theme['slot_report'];
}
public function supportsSlot(string $slot, ?string $slug = null): bool
{
$declared = $this->declaredSlots($slug);
if ($declared === []) {
// Undeclared themes: assume unknown (not supported for soft checks).
return false;
}
return in_array($slot, $declared, true);
}
public function setActive(string $slug): void
{
$themes = $this->discover();
if (! $themes->has($slug)) {
throw new InvalidArgumentException("Theme [{$slug}] was not found.");
}
$this->activeTheme = $slug;
$settings = $this->settings();
if ($settings !== null) {
$settings->active_theme = $slug;
$settings->save();
}
$this->registerViewNamespaces();
}
public function active(): string
{
try {
$settings = app(GeneralSettings::class);
if (filled($settings->active_theme)) {
return $settings->active_theme;
}
} catch (Throwable) {
// Settings may be unavailable or incomplete during migrations.
}
return $this->activeTheme;
}
public function path(?string $slug = null): string
{
$slug ??= $this->active();
return rtrim(config('larablog.theme_path', base_path('themes')), '/').'/'.$slug;
}
public function registerViewNamespaces(): void
{
try {
$themePath = config('larablog.theme_path', base_path('themes'));
$defaultPath = $themePath.'/default';
if (is_dir($defaultPath)) {
View::addNamespace('theme', $defaultPath.'/views');
}
$active = $this->active();
$activePath = $this->path($active).'/views';
if ($active !== 'default' && is_dir($activePath)) {
View::prependNamespace('theme', $activePath);
}
} catch (Throwable) {
// Ignore during incomplete settings/bootstrap states.
}
}
public function assetUrl(string $path): string
{
$trimmed = ltrim($path, '/');
return url('/themes/'.$this->active().'/'.$trimmed);
}
public function ensureActiveThemeExists(): void
{
if (! is_dir($this->path($this->active()))) {
if ($this->active() !== 'default' && is_dir($this->path('default'))) {
$this->activeTheme = 'default';
return;
}
throw new RuntimeException('No valid theme directory found.');
}
}
protected function settings(): ?GeneralSettings
{
try {
return app(GeneralSettings::class);
} catch (Throwable) {
return null;
}
}
}