377 lines
13 KiB
PHP
377 lines
13 KiB
PHP
<?php
|
|
|
|
namespace Modules\Stylist\Providers;
|
|
|
|
use Illuminate\Support\Facades\Blade;
|
|
use Illuminate\Support\Facades\View;
|
|
use Illuminate\Support\ServiceProvider;
|
|
use Modules\Stylist\Console\CreateThemeCommand;
|
|
use Modules\Stylist\Console\FindDuplicateViewsCommand;
|
|
use Modules\Stylist\Console\InspectThemesCommand;
|
|
use Modules\Stylist\Console\PublishAssetsCommand;
|
|
use Modules\Stylist\Console\ThemeDoctorCommand;
|
|
use Modules\Stylist\Console\ValidateThemeCommand;
|
|
use Modules\Stylist\Html\ThemeHtmlBuilder;
|
|
use Modules\Stylist\Html\ThemeSeoBuilder;
|
|
use Modules\Stylist\Support\ModuleInfo;
|
|
use Modules\Stylist\Theme\Loader;
|
|
use Modules\Stylist\Theme\Stylist;
|
|
use Modules\Stylist\Theme\Theme;
|
|
|
|
final class StylistServiceProvider extends ServiceProvider
|
|
{
|
|
protected string $moduleName = 'Stylist';
|
|
|
|
protected string $moduleNameLower = 'stylist';
|
|
|
|
public function register(): void
|
|
{
|
|
$this->app->register(RouteServiceProvider::class);
|
|
|
|
$this->mergeConfigFrom(module_path(ModuleInfo::name(), 'Config/config.php'), ModuleInfo::nameLower());
|
|
|
|
$this->app->singleton('stylist', fn ($app) => $app->make(Stylist::class));
|
|
$this->app->singleton('stylist.html', fn ($app) => $app->make(ThemeHtmlBuilder::class));
|
|
$this->app->singleton('stylist.seo', fn ($app) => $app->make(ThemeSeoBuilder::class));
|
|
}
|
|
|
|
public function boot(): void
|
|
{
|
|
$this->registerViews();
|
|
$this->registerThemeViews();
|
|
$this->registerTranslations();
|
|
$this->registerPublishing();
|
|
$this->registerHelpers();
|
|
$this->registerBlade();
|
|
|
|
if ($this->app->runningInConsole()) {
|
|
$this->commands([
|
|
PublishAssetsCommand::class,
|
|
ValidateThemeCommand::class,
|
|
InspectThemesCommand::class,
|
|
FindDuplicateViewsCommand::class,
|
|
ThemeDoctorCommand::class,
|
|
CreateThemeCommand::class,
|
|
]);
|
|
}
|
|
|
|
$this->loadMigrationsFrom(module_path(ModuleInfo::name(), 'Database/Migrations'));
|
|
|
|
View::composer('*', function ($view): void {
|
|
if (! $this->app->bound(Stylist::class)) {
|
|
return;
|
|
}
|
|
|
|
$view->with('activeTheme', $this->app->make(Stylist::class)->current());
|
|
});
|
|
|
|
// Register both selected presentation trees immediately, then register them again
|
|
// after every service provider has finished booting. Some package/module providers
|
|
// replace Laravel's base view locations during their own boot phase; the booted
|
|
// callback makes Stylist the final authority for ordinary view('...') resolution.
|
|
$this->registerSelectedThemeOverrides();
|
|
|
|
$this->app->booted(function (): void {
|
|
$this->registerSelectedThemeOverrides();
|
|
});
|
|
|
|
}
|
|
|
|
private function registerViews(): void
|
|
{
|
|
$sourcePath = module_path(ModuleInfo::name(), 'Resources/views');
|
|
|
|
// Module-owned package views stay inside the module. Application overrides
|
|
// belong to resources/themes/{theme}/views/modules/stylist, which Stylist
|
|
// registers as the authoritative themed namespace. Never publish back into
|
|
// resources/views; the standard baseline intentionally has no legacy view tree.
|
|
$this->loadViewsFrom($sourcePath, ModuleInfo::nameLower());
|
|
}
|
|
|
|
private function registerThemeViews(): void
|
|
{
|
|
foreach ($this->themeRoots() as $root) {
|
|
if (! is_dir($root)) {
|
|
logger()->warning('Stylist theme root does not exist', [
|
|
'root' => $root,
|
|
]);
|
|
|
|
continue;
|
|
}
|
|
|
|
$themeDirectories = glob($root.'/*', GLOB_ONLYDIR) ?: [];
|
|
|
|
foreach ($themeDirectories as $themeDirectory) {
|
|
$themeViewsPath = $themeDirectory.'/views';
|
|
|
|
if (! is_dir($themeViewsPath)) {
|
|
continue;
|
|
}
|
|
|
|
$themeKey = strtolower(basename($themeDirectory));
|
|
|
|
// Themes always live outside the module, normally in resources/themes.
|
|
// Register the canonical namespace and the legacy namespace as an alias
|
|
// to the same application-owned directory. The alias keeps existing
|
|
// routes and Blade templates working without reintroducing module themes.
|
|
$namespaces = [
|
|
"stylist-theme-{$themeKey}",
|
|
"stylist-module-theme-{$themeKey}",
|
|
];
|
|
|
|
$componentsPath = $themeViewsPath.'/components';
|
|
|
|
// The common theme is the application-wide owner of layouts,
|
|
// authentication, settings and shared components. Livewire 4
|
|
// resolves its default full-page layout through layouts::app,
|
|
// so point that compatibility namespace at the common theme.
|
|
if ($themeKey === 'common') {
|
|
$layoutsPath = $componentsPath.'/layouts';
|
|
|
|
if (is_dir($layoutsPath)) {
|
|
View::addNamespace('layouts', $layoutsPath);
|
|
Blade::anonymousComponentPath($layoutsPath, 'layouts');
|
|
}
|
|
|
|
View::addNamespace('common', $themeViewsPath);
|
|
|
|
if (is_dir($componentsPath)) {
|
|
Blade::anonymousComponentPath($componentsPath, 'common');
|
|
}
|
|
}
|
|
|
|
foreach ($namespaces as $namespace) {
|
|
View::addNamespace($namespace, $themeViewsPath);
|
|
|
|
if (is_dir($componentsPath)) {
|
|
Blade::anonymousComponentPath($componentsPath, $namespace);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private function registerSelectedThemeOverrides(): void
|
|
{
|
|
$stylist = $this->app->make(Stylist::class);
|
|
|
|
$current = $stylist->current();
|
|
$frontend = $stylist->frontend();
|
|
$backend = $stylist->backend();
|
|
|
|
// The active request context must win ordinary view resolution. Keep the
|
|
// opposite presentation tree available as a compatibility fallback, but
|
|
// never let frontend views shadow backend views on backend requests (or
|
|
// vice versa) simply because of provider registration order.
|
|
$orderedThemes = [$current, $frontend, $backend];
|
|
$chains = [];
|
|
$seenThemes = [];
|
|
|
|
foreach ($orderedThemes as $theme) {
|
|
if (isset($seenThemes[$theme->key()])) {
|
|
continue;
|
|
}
|
|
|
|
$seenThemes[$theme->key()] = true;
|
|
$chains[] = $this->themeChain($theme);
|
|
}
|
|
|
|
$keys = [];
|
|
foreach ($chains as $chain) {
|
|
foreach ($chain as $key) {
|
|
if (! in_array($key, $keys, true)) {
|
|
$keys[] = $key;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (! in_array('common', $keys, true)) {
|
|
$keys[] = 'common';
|
|
}
|
|
|
|
// Laravel's starter kits resolve authenticated layouts through the
|
|
// x-layouts:: namespace. Once those views live inside the backend
|
|
// theme, Stylist must make that selected theme authoritative for the
|
|
// namespace as well as for ordinary view('...') lookups.
|
|
$this->registerLayoutCompatibilityNamespace($stylist->backend());
|
|
|
|
$finder = View::getFinder();
|
|
$themePaths = [];
|
|
|
|
foreach ($keys as $key) {
|
|
try {
|
|
$theme = $this->app->make(Loader::class)->find($key);
|
|
} catch (\Throwable) {
|
|
continue;
|
|
}
|
|
|
|
$viewsPath = $theme->path().DIRECTORY_SEPARATOR.'views';
|
|
if (is_dir($viewsPath)) {
|
|
$themePaths[] = $viewsPath;
|
|
}
|
|
|
|
$this->registerThemeModuleNamespaces($key);
|
|
}
|
|
|
|
// FileViewFinder is the source of truth for ordinary view('...') resolution.
|
|
// Set the complete ordered path list explicitly instead of relying on provider
|
|
// boot order and incremental prependLocation() calls. Theme paths stay first;
|
|
// Laravel's normal view locations remain as the final fallback.
|
|
if (method_exists($finder, 'getPaths') && method_exists($finder, 'setPaths')) {
|
|
$existingPaths = $finder->getPaths();
|
|
$paths = array_values(array_unique(array_merge($themePaths, $existingPaths)));
|
|
$finder->setPaths($paths);
|
|
|
|
if (method_exists($finder, 'flush')) {
|
|
$finder->flush();
|
|
}
|
|
} else {
|
|
foreach (array_reverse($themePaths) as $viewsPath) {
|
|
if (method_exists($finder, 'prependLocation')) {
|
|
$finder->prependLocation($viewsPath);
|
|
} else {
|
|
View::addLocation($viewsPath);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/** @return array<int, string> */
|
|
private function themeChain(Theme $theme): array
|
|
{
|
|
$loader = $this->app->make(Loader::class);
|
|
$chain = [];
|
|
$seen = [];
|
|
|
|
while (! isset($seen[$theme->key()])) {
|
|
$seen[$theme->key()] = true;
|
|
$chain[] = $theme->key();
|
|
|
|
$parent = $theme->parent();
|
|
if (! $parent) {
|
|
break;
|
|
}
|
|
|
|
try {
|
|
$theme = $loader->find($parent);
|
|
} catch (\Throwable) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
return $chain;
|
|
}
|
|
|
|
private function prependThemeViewTree(string $themeKey): void
|
|
{
|
|
try {
|
|
$theme = $this->app->make(Loader::class)->find($themeKey);
|
|
} catch (\Throwable) {
|
|
return;
|
|
}
|
|
|
|
$viewsPath = $theme->path().DIRECTORY_SEPARATOR.'views';
|
|
if (! is_dir($viewsPath)) {
|
|
return;
|
|
}
|
|
|
|
$finder = View::getFinder();
|
|
if (method_exists($finder, 'prependLocation')) {
|
|
$finder->prependLocation($viewsPath);
|
|
} else {
|
|
View::addLocation($viewsPath);
|
|
}
|
|
|
|
$this->registerThemeModuleNamespaces($themeKey);
|
|
}
|
|
|
|
private function registerLayoutCompatibilityNamespace(Theme $theme): void
|
|
{
|
|
$candidates = [
|
|
$theme->path().DIRECTORY_SEPARATOR.'views'.DIRECTORY_SEPARATOR.'layouts',
|
|
$theme->path().DIRECTORY_SEPARATOR.'views'.DIRECTORY_SEPARATOR.'components'.DIRECTORY_SEPARATOR.'layouts',
|
|
];
|
|
|
|
foreach ($candidates as $layoutsPath) {
|
|
if (! is_dir($layoutsPath)) {
|
|
continue;
|
|
}
|
|
|
|
View::prependNamespace('layouts', $layoutsPath);
|
|
Blade::anonymousComponentPath($layoutsPath, 'layouts');
|
|
}
|
|
}
|
|
|
|
private function registerThemeModuleNamespaces(string $themeKey): void
|
|
{
|
|
try {
|
|
$theme = $this->app->make(Loader::class)->find($themeKey);
|
|
} catch (\Throwable) {
|
|
return;
|
|
}
|
|
|
|
$modulesPath = $theme->path().DIRECTORY_SEPARATOR.'views'.DIRECTORY_SEPARATOR.'modules';
|
|
if (! is_dir($modulesPath)) {
|
|
return;
|
|
}
|
|
|
|
foreach (glob($modulesPath.'/*', GLOB_ONLYDIR) ?: [] as $moduleViewsPath) {
|
|
$namespace = strtolower(basename($moduleViewsPath));
|
|
View::prependNamespace($namespace, $moduleViewsPath);
|
|
|
|
$componentsPath = $moduleViewsPath.DIRECTORY_SEPARATOR.'components';
|
|
if (is_dir($componentsPath)) {
|
|
Blade::anonymousComponentPath($componentsPath, $namespace);
|
|
}
|
|
}
|
|
}
|
|
|
|
private function normalisePath(string $path): string
|
|
{
|
|
return rtrim(
|
|
str_replace('\\', '/', realpath($path) ?: $path),
|
|
'/'
|
|
);
|
|
}
|
|
|
|
/** @return array<int, string> */
|
|
private function themeRoots(): array
|
|
{
|
|
return array_values(array_unique(array_filter([
|
|
(string) config('stylist.themes_path'),
|
|
])));
|
|
}
|
|
|
|
private function registerTranslations(): void
|
|
{
|
|
$langPath = resource_path('lang/modules/'.ModuleInfo::nameLower());
|
|
$sourcePath = module_path(ModuleInfo::name(), 'Resources/lang');
|
|
|
|
$this->loadTranslationsFrom(is_dir($langPath) ? $langPath : $sourcePath, ModuleInfo::nameLower());
|
|
}
|
|
|
|
private function registerPublishing(): void
|
|
{
|
|
$this->publishes([
|
|
module_path(ModuleInfo::name(), 'Config/config.php') => config_path('stylist.php'),
|
|
], ['config', 'stylist-config']);
|
|
|
|
}
|
|
|
|
private function registerHelpers(): void
|
|
{
|
|
foreach (glob(module_path(ModuleInfo::name(), 'Helpers/*.php')) ?: [] as $filename) {
|
|
require_once $filename;
|
|
}
|
|
}
|
|
|
|
private function registerBlade(): void
|
|
{
|
|
Blade::directive('themeVariables', fn () => '<?php echo stylist_html()->variables(); ?>');
|
|
Blade::directive('themeStylesheet', fn ($expression) => '<?php echo stylist_html()->stylesheet('.($expression ?: 'null').'); ?>');
|
|
Blade::directive('themeScript', fn ($expression) => '<?php echo stylist_html()->script('.($expression ?: 'null').'); ?>');
|
|
Blade::directive('themeSwitcher', fn () => '<?php echo stylist_html()->switcher(); ?>');
|
|
Blade::directive('themeSeo', fn ($expression) => '<?php echo app(\Modules\Stylist\Html\ThemeSeoBuilder::class)->render('.($expression ?: '[]').'); ?>');
|
|
}
|
|
}
|