⌘K

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-settings
php artisan wire-module-settings:install
php 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();
}
});

Související

  • Moduly — jak balíček dodává takovouhle oblast
  • Resources — katalog, ve kterém je tahle obrazovka registrovaná
  • Formuláře — komponenty, kterými se píše schéma skupiny