> */ 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 */ 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 */ 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; } } }