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
- Jak to funguje
- Čtení nastavení
- Výchozí hodnoty patří skupině
- Ikony, popisky a pořadí
- Balíček může dodat vlastní tab
- Úprava hodnot cestou tam i zpátky
- Pravidla napříč poli
- Jedna obrazovka nad několika úložnými skupinami
- Kreslení z živého stavu
- Vlastní obrazovka
- Na vlastní stránce
- Kdo co smí měnit
- Reakce na změnu
- Kde se to cachuje
- Zbytek souboru
- Rozsáhlý příklad
- Související
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.
Úprava hodnot cestou tam i zpátky
Pole formuláře má vlastní tvar a kód, který nastavení čte, obvykle chce užší:
výběr času odpoví 08:30 a stopky čtou 08:30:00, sada zaškrtávátek odpoví tím,
co bylo zaškrtnuté, v pořadí, v jakém to bylo zaškrtnuté, a čtenář chce jen
případy, které zná enum. TransformsSettings je švem mezi nimi:
use NyonCode\WireModuleSettings\Contracts\SettingsGroup;use NyonCode\WireModuleSettings\Contracts\TransformsSettings; final class ShiftSettings implements SettingsGroup, TransformsSettings{ // group(), label(), schema() … public static function fromStorage(array $values): array { return [...$values, 'start' => substr((string) ($values['start'] ?? ''), 0, 5)]; } public static function toStorage(array $data): array { return [...$data, 'start' => $data['start'].':00']; } }
fromStorage() dostane uložené hodnoty s výchozími hodnotami skupiny pod nimi
a vrátí stav formuláře. toStorage() běží až po tom, co formulář upravil svá
pole — po validaci a s nahraným souborem FileUpload už přesunutým na disk —
a vrátí to, co se zapíše: klíč, který vynechá, se neuloží, klíč, který přidá,
ano. Tady také nahraný soubor dostanete kamkoli vlastního, protože pole předává
jeho cestu.
Pravidla napříč poli
Pravidla pole vidí jen pole. „Počáteční číslo potřebuje rok, ve kterém začíná“ se týká dvou polí a jeho zpráva patří k jednomu z nich, ne do toastu:
use NyonCode\WireModuleSettings\Contracts\ValidatesSettings; public static function validateSettings(array $data): array{ return blank($data['start']) === blank($data['start_year']) ? [] : ['start' => __('Vyplňte číslo i jeho rok, nebo ani jedno.')]; }
Volá se po tom, co prošla všechna pravidla polí, s validovaným stavem. Prázdná odpověď uloží; zpráva uložení zastaví — nic se nezapíše — a zobrazí se pod polem, jehož jménem je klíčovaná.
Jedna obrazovka nad několika úložnými skupinami
Úložné skupiny se cachují a oznamují jedna po druhé, takže se řídí kódem, který
je čte; obrazovka se řídí člověkem, který je mění. Když se ti dva neshodnou —
stopky čtou timers, výkazy čtou reports a obojí je „jak se vykazuje práce“ —
jedna skupina pokryje obě:
use NyonCode\WireModuleSettings\Contracts\SpansSettingsGroups; public static function storage(): array{ return ['timers' => ['check_interval', 'shift_start', 'shift_end']]; }
Vyjmenovaný klíč se čte z úložné skupiny, pod kterou je uvedený, a do ní se i
zapisuje; každý jiný klíč zůstává ve vlastní skupině. Uložení zapíše všechny
v jedné transakci přes Settings::fillMany() a ohlásí každou úložnou
skupinu, kterou zapsalo. Klíč, který zatím nikdo neuložil, si drží výchozí
hodnotu, kterou pro něj skupina deklaruje.
Settings::fillMany([ 'reports' => ['format' => 'decimal'], 'timers' => ['check_interval' => 15],]);
Kreslení z živého stavu
Některá nastavení se nastavují snáz, když je vidět, co dělají — číslo, které tvar vyrobí, kolik sazba vyplatí. To není pole, ale čtení polí, takže ho skupina vykreslí ze stavu, který formulář drží právě teď:
use Illuminate\Contracts\Support\Htmlable;use Illuminate\Support\HtmlString;use NyonCode\WireModuleSettings\Contracts\ExtendsSettingsScreen; public static function screenExtension(array $data): ?Htmlable{ return new HtmlString(e(OfferNumber::preview($data['format'] ?? ''))); }
Kreslí se pod formulářem při každém vykreslení, takže pole s live(), které
čte, ho překreslí, jak se hodnota mění. Čte a nikdy nezapisuje — ukládání patří
formuláři.
Vlastní obrazovka
Úniková cesta pro to, co formulář nepopíše — kalkulačka s vlastním stavem, spojení, které je potřeba otestovat před uložením. Skupina pojmenuje Livewire komponentu a modul ji vykreslí místo formuláře; přepínač, nadpis, adresa i to, kdo ji smí otevřít, zůstávají jeho:
use NyonCode\WireModuleSettings\Contracts\RendersSettingsScreen; public static function component(): string{ return WageSettingsScreen::class; // připojená s názvem skupiny jako `group` }
Komponenta ukládá přes Settings jako jakýkoli jiný kód; vlastní uložení stránky
takovou skupinu odmítne (404), takže se jejím jménem nic nezapíše. schema()
může klidně vrátit [].
Skupina může také vlastními slovy potvrdit uložení, na čemž záleží, jakmile je skupin víc — „Nastavení uloženo.“ neřekne, které:
use NyonCode\WireModuleSettings\Contracts\ConfirmsSettingsSave; public static function savedMessage(): string{ return __('Nastavení nabídek uloženo.');}
Na vlastní stránce
Aplikace, která už sekci nastavení má — vlastní routu, položku v menu, drobečky
a velmi pravděpodobně i klíč settings — si ji nechá a postaví ji na modulu.
Vypněte vlastní obrazovku modulu; úložiště, skupiny i všechny kontrakty dál
fungují a pod tím klíčem se nic nezaregistruje:
// config/wire-module-settings.php'screen' => false, // jen úložiště a skupiny — žádná routa, žádná položka v menu 'switcher' => 'tabs', // odkazy na skupiny jako lišta záložek
Potom routujte SettingsPage nebo vlastní třídu, která z ní dědí, tam, kde vaše
sekce už je. Proto stránka není final: přepište getTitle() a breadcrumbs()
pro vlastní drobečky, groups() pro to, které skupiny stránka nabízí, a
groupUrl() pro vlastní adresu. Když modul nemá vlastní routu, na kterou by
odkázal, skupiny se přepínají na stránce samotné — ?group=mail, pořád jedna
adresa na skupinu, takže záložka v prohlížeči na ni dál vede.
width omezí, jak široká obrazovka skupiny naroste, a vycentruje ji — jedna ze
šířek, které bere modal (sm … 7xl, full), takže 2xl je tatáž šířka jako
tam; null nechá celou šířku stránky. Sloupec krátkých polí roztažený přes široký
monitor se čte jako řada prázdných krabic. actions_alignment umístí tlačítko
uložení (left, center, right). Neznámá šířka vyhodí výjimku, stejně jako
neznámý přepínač.
switcher bere links (řadu tlačítek, výchozí) nebo tabs (lištu, jakou kreslí
<x-wire::tabs>); obojí jsou odkazy. Cokoli jiného vyhodí
SettingsScreenException, protože překlep, který by potichu nakreslil odkazy,
by se četl jako „záložky nejsou podporované“.
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á.
Odebrání je taky zápis. Settings::remove() a Settings::clear() vyšlou tutéž
událost, kde každý odebraný klíč nese to, co za něj odpovídá teď — deklarovanou
výchozí hodnotu skupiny, nebo null — takže posluchač, který přestavuje mailový
transport, uslyší smazání vlastního hostitele stejně jako jeho nastavení.
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.
Skupina se cachuje navždy a zahazuje ji model. Uložení nebo smazání
Setting skupinu vyčistí, což pokryje obrazovku, Settings::set(), seeder
i factory. Zápis přes query builder není zápis přes model a nic nevyvolá, takže
cache dál na každém workeru odpovídá starou hodnotou. Po takovém zápisu skupinu
vyčistěte sami:
Setting::query()->where('group', 'branding')->update([...]); Settings::forget('branding');
Zbytek souboru
Všechny 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 'screen' => true, // false: jen úložiště — viz Na vlastní stránce'switcher' => 'links', // nebo 'tabs''width' => null, // nebo šířka modalu: 'sm' … '7xl', 'full''actions_alignment' => 'left','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í, a jeho migrace vytvoří právě tu.
Nastavte ji před migrací — přejmenování potom namíří modul na tabulku, která
ještě neexistuje, a každé čtení pak odpoví výchozími hodnotami. php artisan about tabulku, kterou modul používá, vypíše. 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(); }});