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.
This commit is contained in:
@@ -0,0 +1,192 @@
|
||||
<?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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user