Files
livewire-material/src/LivewireMaterialServiceProvider.php
T
Andreas Reinhold / reiniandClaude Opus 5 fb7007c976
tests / feature (8.4) (push) Successful in 2m0s
tests / feature (8.5) (push) Successful in 2m0s
tests / browser (chrome, chromium) (push) Failing after 8m3s
tests / browser (firefox, firefox) (push) Failing after 12m58s
tests / browser (safari, webkit) (push) Failing after 13m8s
Take Tailwind out of the package, and its detection out of the guard
Tailwind left the stack in 2.0.0, but the package still carried about 330
mentions of it. What the guard's Tailwind detection protected — a class
that compiles to nothing — is now protected by a check that does not care
where a dead class came from.

DesignGuard: about 500 lines of Tailwind tables, scales, palettes and
"2.0.0 replacement" hints give way to one check — a class a view or PHP
file writes that neither the application's stylesheets nor the package's
own declare. It catches a utility of any framework, a typo and a class
whose rules were deleted alike, so it also found two classes ReStride
draws nothing with. A stylesheet has to be in reach for it: the `.css`
files among the scanned paths, or what the `missingStylesheets()` entry
imports. The class reader no longer mistakes an array index for a class
list (`$block['base']`), and it reads the array a class helper is given,
where it read nothing before.

The package's own three Tailwind self-guards go with it. Only their one
unique check stays, as a test of its own: every `matchMedia` width in
resources/js is an M3 breakpoint.

The pagination views are `material.blade.php` and
`simple-material.blade.php`; only Laravel's and Livewire's default theme
names ever made them `tailwind`. The provider sets `Paginator`'s default
views and switches `livewire.pagination_theme` to `material` when it is
still Livewire's own default, so no application can forget the config; a
theme an application chose, and a component's own `$paginationTheme` or
`paginationView()`, still win.

The rest is prose: the layer-order guidance for an application that still
builds Tailwind, the Tailwind wording in the README, the Boost guidelines
and the development skill, and about 25 "this used to be a Tailwind
utility" comments, along with every "plan step NN" pointer into a
gitignored folder. The reset keeps its credit, and NOTICE now carries it
too.

Feature suite 1159 passed, Chrome browser suite 299 passed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-17 21:07:39 +02:00

216 lines
8.9 KiB
PHP

<?php
namespace NoNameWeb\LivewireMaterial;
use Illuminate\Contracts\View\Factory;
use Illuminate\Pagination\Paginator;
use Illuminate\Support\Facades\Blade;
use Illuminate\Support\Facades\Route;
use Illuminate\Support\ServiceProvider;
class LivewireMaterialServiceProvider extends ServiceProvider
{
public function register(): void
{
$this->mergeConfigFrom(__DIR__.'/../config/livewire-material.php', 'livewire-material');
$this->disableBladeIconsComponent();
$this->registerErrorViews();
$this->registerMailComponents();
}
public function boot(): void
{
$this->loadViewsFrom(__DIR__.'/../resources/views', 'livewire-material');
$this->registerComponents();
$this->registerPagination();
$this->registerShowcase();
if ($this->app->runningInConsole()) {
$this->commands([
Console\SchemeCommand::class,
]);
$this->publishes([
__DIR__.'/../config/livewire-material.php' => config_path('livewire-material.php'),
], 'livewire-material-config');
$this->publishes([
static::errorViewPath().'/errors' => resource_path('views/errors'),
], 'livewire-material-errors');
$this->publishes([
static::mailComponentPath().'/html' => resource_path('views/vendor/mail/html'),
static::mailComponentPath().'/text' => resource_path('views/vendor/mail/text'),
], 'livewire-material-mail');
}
}
/**
* The anonymous components.
*/
public static function componentPath(): string
{
return __DIR__.'/../resources/views/components';
}
/**
* The error-view root: a folder holding only `errors/`.
*/
public static function errorViewPath(): string
{
return dirname(__DIR__).'/resources/views/error-pages';
}
/**
* The mail component root, laid out like the framework's: `html/` and `text/`.
*/
public static function mailComponentPath(): string
{
return dirname(__DIR__).'/resources/views/mail';
}
/**
* Append the error-view root to `view.paths`, after the application's own.
*
* The exception handler replaces the `errors` namespace each time it renders an HTTP
* exception, with every entry of `view.paths` plus `/errors` and the framework's views last
* (RegisterErrorViewPaths), so a namespace added with addNamespace() is gone by then; only a
* view path survives. That config is read at render time, but also once when the view
* finder is built: appended here, in register(), before any boot() resolves the view
* factory, the finder and `view:cache` see the same paths the handler does. Appending keeps
* `view.paths[0]` — Laravel's viewPath() — the application's, and puts the application's
* own `resources/views/errors` first. Idempotent, since a cached config already holds it.
*/
protected function registerErrorViews(): void
{
$paths = (array) config('view.paths', []);
if (! in_array(static::errorViewPath(), $paths, true)) {
config(['view.paths' => [...$paths, static::errorViewPath()]]);
}
}
/**
* Put the package's mail header and message after the application's own mail components,
* when `livewire-material.mail.components` asks for it. Off by default: it changes every
* Markdown mail the application sends, theme or not, and the framework reads
* `mail.markdown.paths` once, when the Markdown renderer is first made — after register().
*/
protected function registerMailComponents(): void
{
if (! config('livewire-material.mail.components')) {
return;
}
$paths = (array) config('mail.markdown.paths', []);
if (! in_array(static::mailComponentPath(), $paths, true)) {
config(['mail.markdown.paths' => [...$paths, static::mailComponentPath()]]);
}
}
/**
* Register the components as anonymous Blade components under the configured prefix, or
* under `livewire-material` without one.
*
* A compiled `<x-button>` names the view namespace Blade derives from the prefix, or from the
* component folder's absolute path when there is none. That path differs between a laptop and
* a container sharing `storage/framework/views`, or between release directories, while the
* compiled file's name does not — so a view compiled on one showed `a1b2…::button` as text on
* the other. A prefix does not stop an unprefixed tag resolving.
*
* The package's own views never go through this registration: they write
* `<x-livewire-material::button>`, which resolves through the view namespace whatever the
* prefix, and which an application's own `<x-button>` cannot shadow.
*/
protected function registerComponents(): void
{
Blade::anonymousComponentPath(
static::componentPath(),
filled(config('livewire-material.prefix')) ? config('livewire-material.prefix') : 'livewire-material',
);
}
/**
* Put the M3 paginators in front of Laravel's and Livewire's own, and take over both
* frameworks' default theme so no application forgets the config and silently renders
* Laravel's or Livewire's own Tailwind view instead.
*
* The namespaces still go through callAfterResolving('view', …): the Factory instance only
* exists once the `view` service resolves, which may happen before or after this method runs.
* `Paginator::defaultView()`/`defaultSimpleView()` and `livewire.pagination_theme`, in
* contrast, are a plain static and a config value — nothing needs resolving first — so they
* are set here directly, during this provider's boot(). Package providers boot before the
* application's own (its `bootstrap/providers.php` list is appended after the discovered
* package providers), so an application that calls `Paginator::defaultView()` again, or sets
* `livewire.pagination_theme` itself, in its own boot() still wins. Deferring them into the
* callback instead would risk the opposite: `view` can resolve arbitrarily late — the first
* render, possibly after the application's own boot() already set its override — and this
* call would silently overwrite it.
*
* `livewire.pagination_theme` is only taken over while it still reads as Livewire's own
* default, `tailwind` — including when the key is missing entirely, since
* `SupportPagination::paginationView()` reads it with that same fallback. An application that
* chose another theme, or already set `material`, keeps it. Either way, a component's own
* `$paginationTheme` property or `paginationView()` method still wins over the config, because
* Livewire checks those first.
*/
protected function registerPagination(): void
{
if (! config('livewire-material.pagination')) {
return;
}
$this->callAfterResolving('view', function (Factory $view): void {
$view->prependNamespace('pagination', __DIR__.'/../resources/views/pagination/laravel');
$view->prependNamespace('livewire', __DIR__.'/../resources/views/pagination/livewire');
});
Paginator::defaultView('pagination::material');
Paginator::defaultSimpleView('pagination::simple-material');
if (config('livewire.pagination_theme', 'tailwind') === 'tailwind') {
config(['livewire.pagination_theme' => 'material']);
}
}
/**
* blade-icons registers a class-based <x-icon> of its own, and Blade resolves a
* registered class alias before any anonymous component path, so ours would never
* render. It reads the key in its boot(); a booting callback runs after every
* register() — so after blade-icons merges its defaults over the key — and before
* any provider boots.
*/
protected function disableBladeIconsComponent(): void
{
$this->app->booting(function (): void {
config(['blade-icons.components.default' => null]);
});
}
/**
* Mount the showcase when it is enabled.
*/
protected function registerShowcase(): void
{
// Registered even with the showcase off: `php artisan view:cache` compiles every view in the
// package's namespace, the showcase's pages included, and <x-showcase::…> must resolve there.
Blade::anonymousComponentPath(__DIR__.'/../resources/views/showcase/components', 'showcase');
if (! config('livewire-material.showcase.enabled')) {
return;
}
if ($this->app->routesAreCached()) {
return;
}
Route::middleware(config('livewire-material.showcase.middleware'))
->prefix(config('livewire-material.showcase.path'))
->name('livewire-material.')
->group(__DIR__.'/../routes/showcase.php');
}
}