Modules
Modul Nastavení
Typované nastavení aplikace — úložiště, cache a obrazovku dodává modul, co je nastavitelné určuje aplikace.
Na této stránce
Každá aplikace nakonec má hrstku věcí, které chce někdo změnit bez nasazení. Tohle je pro ně tabulka, cache a obrazovka.
composer require nyoncode/wire-module-settingsphp artisan wire-module-settings:installphp artisan migrate
Jak to funguje
Modul vlastní úložiště a obrazovku; aplikace vlastní to, co je nastavitelné. Balíček, který by dodával vlastní seznam nastavení, by hádal, co aplikace potřebuje — skupina je proto třída, kterou napíšete vy:
use NyonCode\WireModuleSettings\Contracts\SettingsGroup; final class BrandingSettings implements SettingsGroup { public static function group(): string { return 'branding'; } public static function label(): string { return __('Branding'); } public static function schema(): array { return [TextInput::make('company_name'), Toggle::make('dark_default')]; }}
// config/wire-module-settings.php'groups' => [App\Settings\BrandingSettings::class],
Hodnoty si drží svůj typ. Sloupec je JSON, takže boolean se vrátí jako boolean a pole přežije — tabulka nastavení, která všechno převede na řetězec, nutí každého čtenáře přetypovávat ručně, a vám se pak neshodnou.
Skupina je jeden záznam v cache. Nastavení se čte skoro při každém requestu a
zapisuje se téměř nikdy; po klíčích by to bylo jedno hledání na čtení, po
skupinách je to jedno pro stránku, která přečte šest hodnot. Každý zápis záznam
skupiny zahodí — včetně zápisu přes model, tedy seederu, datové migrace nebo
tinkeru — protože invalidace patří tam, kde se zapisuje, ne jen do Settings.
Čtení je bezpečné i dřív, než tabulka existuje, a ta odpověď se nepamatuje.
Volání nastavení sedí v kódu, který běží i během migrate a při čisté instalaci,
takže chybějící tabulka odpoví výchozí hodnotou místo výjimky. Záměrně se
necachuje: rememberForever znamená navždy, a nasazení, které sáhlo na nastavení
před migrate, by jinak nechalo aplikaci odpovídat „prázdno“ do chvíle, než
někdo něco zapíše.
Skupina se uloží celá, nebo vůbec. Settings::fill() je jedna transakce, a
je to tentýž slib, na kterém stojí obrazovka: edituje jednu skupinu naráz, aby
nikdy nemusela říct „branding se uložil, ale mail ne“ — a smyčka šesti zápisů, ve
které selže čtvrtý, přesně tohle o polích jedné skupiny říká.
Skupina je URL. Routa je settings/{record}, takže settings/mail otevře
nastavení mailu a člověk si ho může uložit do záložek. Proto je přepínač tvořen
odkazy, a ne taby — stav tabu žijící v Livewire snapshotu do záložek uložit
nejde. Skupina, kterou tato aplikace nedeklaruje, je 404, a skupina, na kterou
tento uživatel nemá, je 403: alternativou by v obou případech byl formulář bez
polí, a prázdno se čte jako „tady není co nastavovat“.
Čtení nastavení
use NyonCode\WireModuleSettings\Support\Settings; Settings::get('company_name', 'Acme', 'branding');Settings::set('dark_default', true, 'branding');Settings::fill(['company_name' => 'Acme', 'dark_default' => true], 'branding');Settings::has('dark_default', 'branding');Settings::remove('dark_default', 'branding');Settings::clear('branding');Settings::all('branding');
get() se rozhoduje v jednom pořadí a stojí za to ho znát: uložená hodnota,
potom výchozí hodnota deklarovaná skupinou, potom $default, který jste
předali. Ten poslední je záchrana pro klíč, který skupina nikdy nedeklarovala.
Výchozí hodnoty patří skupině
Alternativou je výchozí hodnota žijící na každém místě volání — tentýž
'noreply@example.com' napsaný na šesti místech, z nichž se pět shodne. Skupina
je deklaruje jednou, implementací jednoho dalšího rozhraní:
use NyonCode\WireModuleSettings\Contracts\ProvidesSettingsDefaults;use NyonCode\WireModuleSettings\Contracts\SettingsGroup; final class MailSettings implements SettingsGroup, ProvidesSettingsDefaults{ public static function group(): string { return 'mail'; } public static function label(): string { return __('Mail'); } public static function schema(): array { return [TextInput::make('from_address')->email(), Toggle::make('queue')]; } public static function defaults(): array { return ['from_address' => 'noreply@example.com', 'queue' => true]; } }
Settings::get('from_address', null, 'mail') teď odpoví deklarovanou hodnotou —
a stejně tak obrazovka: pole mají správné hodnoty hned při prvním otevření místo
stránky prázdných inputů.
has() je otázka, na kterou výchozí hodnota neodpovídá: jestli někdo hodnotu
skutečně uložil. Datová migrace, která tabulku prochází, chce právě tuhle.
Ikony, popisky a pořadí
Druhé volitelné rozhraní, pro skupinu, která chce nějak vypadat:
use NyonCode\WireModuleSettings\Contracts\DescribesSettingsGroup; final class MailSettings implements SettingsGroup, DescribesSettingsGroup{ // … public static function icon(): ?string { return 'outline:envelope'; } public static function description(): ?string { return __('Odkud odchází transakční pošta.'); } public static function sort(): int { return 20; } }
Ikona je v přepínači vedle skupiny, popisek pod nadpisem a sort() rozhoduje o
pořadí. Skupiny, které pořadí nedeklarují, si drží to, v jakém je vyjmenoval
config — řazení je stabilní, takže skupina, která číslo uvede, nepromíchá ty,
které mlčí. Jediná deklarovaná skupina žádný přepínač nevykreslí: jedna skupina
není volba.
Kartu pro ploché schéma dodá stránka. Každý formulář resource dostává svůj
podklad ze Sectionů, které resource deklaroval — a skupina nastavení, jejímž
celým kontraktem je nadpis a seznam polí, žádný deklarovat nemusí; vykreslená
holá by měla inputy přímo na pozadí stránky, což nedělá žádná jiná obrazovka
panelu. Stránka je proto obalí, a jen tam, kde obal chybí: skupina, která si
přinese vlastní Section, Grid nebo Tabs, se vykreslí tak, jak je — karta
kolem karty je rámeček uvnitř rámečku a skupina už řekla, kde má vlastní okraje.
Balíček může dodat vlastní tab
groups je seznam aplikace a zůstává jím — majitel panelu říká, co se v jeho
panelu nastavuje. Druhá půlka je balíček, který dodává funkci a tab, kterým se
nastavuje; ten jinak musí končit README větou „a teď si tuhle třídu přidejte do
configu“ — jedinou instrukcí, kterou každá jiná plocha v tomhle frameworku už
dávno dávat přestala. Modul se registruje sám, a jeho nastavení taky.
use NyonCode\LaravelPackageToolkit\Packager;use NyonCode\WireModuleSettings\Support\SettingsRegistry; public function configure(Packager $packager): void{ $packager ->name('AcmeBilling') ->hasShortName('acme-billing') ->hasConfig() ->bootedPackage(function (): void { SettingsRegistry::instance()->register(BillingSettings::class); });}
Nic jiného se nemění: je to tentýž SettingsGroup, jaký píše aplikace, se
stejnými volitelnými kontrakty a ukládáním do stejné tabulky.
Kdo vyhraje. Seznam aplikace se čte první, takže skupina, kterou deklaruje
pod stejným úložným názvem, tu dodanou nahradí — a mezi skupinami bez sort()
se řadí před ni. Tab, který nechce vůbec, patří do except:
// config/wire-module-settings.php'except' => ['billing'],
Tahle úniková cesta je důvod, proč balíček vůbec smí tab dodávat — obrazovka dodaná balíčkem, kterou aplikace nemůže odstranit, je přesně to, kvůli čemu lidé přestanou takové balíčky instalovat.
Registrujte v boot, a bude to fungovat i z register. Pořadí providerů je
v Laravelu pořadí, v jakém je našel composer, ne kontrakt — provider
přispívajícího balíčku tedy klidně poběží dřív než provider tohohle modulu.
SettingsRegistry::instance() se naváže při prvním doteku, takže ten, kdo je
tam první, vytvoří jedinou instanci a druhý ji najde. Bez toho by registrace
skončila v zahozeném objektu a tab by na některých strojích chyběl podle
lockfilu.
Kdo co smí měnit
Dvě úrovně a obě jsou Gate::allows() — nic tady kontrolu oprávnění
neimplementuje znovu, takže vlastní gates Laravelu, spatie/laravel-permission
i nyoncode/laravel-permission-extended na ně odpovídají stejně jako na každou
jinou kontrolu v tomhle frameworku.
Obrazovku hlídá jeden řádek konfigurace. Stane se z něj middleware can: na
obou routách a schová položku menu, která k nim vede:
// config/wire-module-settings.php'permission' => 'settings.manage',
Ve výchozím stavu null, protože oprávnění vymyšlené tímhle balíčkem by obrazovku zamklo v každé instalaci, která takovou schopnost nemá. Obrazovka nastavení je hned po auditním logu ta, u které se ho vyplatí pojmenovat nejvíc: mění se na ní chování aplikace bez nasazení.
Jedna skupina může chtít víc než obrazovka:
use NyonCode\WireModuleSettings\Contracts\GuardsSettingsGroup; final class BillingSettings implements SettingsGroup, GuardsSettingsGroup{ // … public static function permission(): ?string { return 'settings.billing'; }}
Skupina, na kterou aktuální uživatel nemá, není v přepínači, otevření její URL je 403 a její uložení také — název skupiny je veřejná property, takže jede v Livewire snapshotu a vrací se z prohlížeče, a uživatel, který smí otevřít jednu skupinu, nesmí uložit jinou tím, že přepíše hodnotu, která cestuje.
Uživatel, který neprojde žádnou deklarovanou skupinou, dostane 403 i na celou obrazovku, a ne prázdný stav: ten říká „deklarujte třídu SettingsGroup a uveďte ji v configu“, což je instrukce pro vývojáře a pro všechny ostatní lež. Aplikace, která skutečně nedeklarovala nic, ho vidí dál.
Reakce na změnu
Nastavení jsou hodnoty, ze kterých se konfiguruje něco dalšího, takže změna jedné
z nich obvykle musí někam dosáhnout. Po každém zápisu se odešle SettingsSaved:
use Illuminate\Support\Facades\Event;use NyonCode\WireModuleSettings\Events\SettingsSaved; Event::listen(SettingsSaved::class, function (SettingsSaved $event): void { if ($event->group === 'mail') { Cache::forget('mail-transport'); }});
$event->values je to, co nesl tenhle zápis, ne celá skupina; posluchač, který
chce zbytek, se zeptá Settings::all() — ta už tyhle hodnoty v odpovědi má.
Kde se to cachuje
// config/wire-module-settings.php'cache' => [ 'enabled' => env('WIRE_SETTINGS_CACHE', true), 'store' => env('WIRE_SETTINGS_CACHE_STORE'),],
Na úložišti záleží víc, než to vypadá. Ponechání null použije výchozí úložiště
aplikace, a v aplikaci, jejíž výchozí je database, se čtení, kterému má tahle
cache předejít, jen vymění za jiný dotaz. Pojmenujte paměťové úložiště, které už
provozujete, a čtení nastavení přestane sahat do databáze — instalátor to řekne,
když najde výchozí database.
Vypnutí cache je na ladění a na testy, které kontrolují přímo tabulku.
Zbytek souboru
Čtyři klíče; každý má sekci výš, kromě těch dvou na konci:
// config/wire-module-settings.php'groups' => [], // vaše vlastní třídy skupin — viz Jak to funguje'except' => [], // úložná jména dodaných tabů, které se mají zahodit 'permission' => null, // oprávnění, které obrazovka vyžaduje — viz Kdo co smí měnit'table' => 'wire_settings', 'navigation' => [ 'group' => 'system', 'label' => null, // null použije vlastní nadpis skupiny modulu 'icon' => 'outline:cog-6-tooth', 'sort' => 97,],
table je tu pro aplikaci, která už tabulku settings měla, když tahle přišla;
modul čte a zapisuje jen do té, o které ví. navigation rozhoduje, kde položka
v menu sedí, ne kdo na ni smí kliknout — to je permission.
Texty jsou publikovatelný překladový soubor a markup publikovatelný pohled —
wire-module-settings::translations a …::views; co to stojí, říká
Vzhled → Lokalizace a
Přepis pohledů.
Rozsáhlý příklad
Skupina, která používá všechna tři volitelná rozhraní, a něco, co ji čte:
namespace App\Settings; use App\Support\Branding;use Illuminate\Support\Facades\Event;use NyonCode\WireForms\Components\TextInput;use NyonCode\WireForms\Components\Toggle;use NyonCode\WireModuleSettings\Contracts\DescribesSettingsGroup;use NyonCode\WireModuleSettings\Contracts\GuardsSettingsGroup;use NyonCode\WireModuleSettings\Contracts\ProvidesSettingsDefaults;use NyonCode\WireModuleSettings\Contracts\SettingsGroup;use NyonCode\WireModuleSettings\Events\SettingsSaved;use NyonCode\WireModuleSettings\Support\Settings; final class BrandingSettings implements DescribesSettingsGroup, GuardsSettingsGroup, ProvidesSettingsDefaults, SettingsGroup{ public static function group(): string { return 'branding'; } public static function label(): string { return __('settings.branding'); } public static function schema(): array { return [ TextInput::make('company_name')->label(__('settings.company_name'))->required(), TextInput::make('support_email')->label(__('settings.support_email'))->email(), Toggle::make('dark_by_default')->label(__('settings.dark_by_default')), ]; } public static function icon(): ?string { return 'outline:swatch'; } public static function description(): ?string { return __('settings.branding_description'); } public static function sort(): int { return 10; } public static function defaults(): array { return ['company_name' => config('app.name'), 'dark_by_default' => false]; } public static function permission(): ?string { return 'settings.branding'; } }
// View composer, mailable, hlavička PDF — kdekoliv je hodnota potřeba.Settings::get('company_name', group: 'branding'); // A cache, kterou si drží něco jiného, zahozená ve chvíli, kdy se hodnota hne.Event::listen(SettingsSaved::class, function (SettingsSaved $event): void { if ($event->group === 'branding') { Branding::flush(); }});