Captures the current working tree after theme slots, ArticleAccess, and the payment / paid-content plugins so subsequent work has a reviewable git history.
193 lines
5.1 KiB
PHP
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;
|
|
}
|
|
}
|
|
}
|