Files
2026-08-15 21:59:18 +01:00

148 lines
4.4 KiB
PHP

<?php
namespace Modules\Stylist\Diagnostics;
use Illuminate\Filesystem\Filesystem;
use Modules\Stylist\Theme\Loader;
use Modules\Stylist\Theme\Theme;
final class ThemeInspector
{
public function __construct(
private readonly Filesystem $files,
private readonly Loader $loader,
) {}
/** @return array<int, array<string, mixed>> */
public function themes(): array
{
return $this->loader->all()->map(function (Theme $theme): array {
return [
'key' => $theme->key(),
'name' => $theme->name(),
'parent' => $theme->parent(),
'path' => $theme->path(),
'views' => count($this->viewFiles($theme)),
'entry_view' => $theme->entryView(),
];
})->values()->all();
}
/** @return array<string, array<int, array{theme:string,path:string}>> */
public function duplicateViews(): array
{
$ownership = [];
foreach ($this->loader->all() as $theme) {
foreach ($this->viewFiles($theme) as $relative => $absolute) {
$ownership[$relative][] = [
'theme' => $theme->key(),
'path' => $absolute,
];
}
}
return array_filter($ownership, fn (array $owners): bool => count($owners) > 1);
}
/** @return array<int, array{severity:string,theme:string,message:string}> */
public function issues(): array
{
$issues = [];
$themes = $this->loader->all();
foreach ($themes as $theme) {
$parent = $theme->parent();
if ($parent !== null && ! $themes->has($parent)) {
$issues[] = [
'severity' => 'error',
'theme' => $theme->key(),
'message' => "Parent theme [{$parent}] is not installed.",
];
}
if ($this->hasInheritanceCycle($theme, $themes->all())) {
$issues[] = [
'severity' => 'error',
'theme' => $theme->key(),
'message' => 'Theme inheritance contains a cycle.',
];
}
$entry = $theme->path().'/views/'.str_replace('.', '/', $theme->entryView()).'.blade.php';
if (! $this->files->exists($entry) && $parent === null) {
$issues[] = [
'severity' => 'warning',
'theme' => $theme->key(),
'message' => "Entry view [{$theme->entryView()}] does not exist in this theme.",
];
}
}
foreach ($this->duplicateViews() as $view => $owners) {
$issues[] = [
'severity' => 'error',
'theme' => implode(', ', array_column($owners, 'theme')),
'message' => "View [{$view}] has multiple owners.",
];
}
$default = (string) config('stylist.default_theme');
if ($default !== '' && ! $themes->has($default)) {
$issues[] = [
'severity' => 'warning',
'theme' => $default,
'message' => 'Configured default theme is not installed.',
];
}
return $issues;
}
/** @return array<string, string> */
private function viewFiles(Theme $theme): array
{
$root = $theme->path().'/views';
if (! $this->files->isDirectory($root)) {
return [];
}
$views = [];
foreach ($this->files->allFiles($root) as $file) {
if (! str_ends_with($file->getFilename(), '.blade.php')) {
continue;
}
$absolute = str_replace('\\', '/', $file->getPathname());
$relative = ltrim(str_replace(str_replace('\\', '/', $root), '', $absolute), '/');
$views[$relative] = $absolute;
}
ksort($views);
return $views;
}
/** @param array<string, Theme> $themes */
private function hasInheritanceCycle(Theme $theme, array $themes): bool
{
$seen = [];
$key = $theme->key();
while (isset($themes[$key])) {
if (isset($seen[$key])) {
return true;
}
$seen[$key] = true;
$key = $themes[$key]->parent() ?? '';
if ($key === '') {
return false;
}
}
return false;
}
}