Panels
Routování
Jak z deklarovaných stránek vzniknou skutečné URL — makro, tvar URL, middleware pro každý resource, několik zón nad jednou sadou resourců a cesta přes config.
Na této stránce
Registr nevlastní žádný URL shell ani routu: routy zůstávají aplikaci, v její
vlastní skupině, s jejím prefixem a middlewarem. Co framework odstraňuje, je
opakování — čtyři řádky Route::get() na každý resource a vedle nich ručně psaná
mapa klíč→URL pro menu.
Jak to funguje
Resource řekne, které stránky ho vykreslují — a stejně tak cokoli dalšího, co aplikace zaregistrovala, včetně dashboardu: router čte tentýž katalog jako menu, takže routovatelnost je věcí deklarace stránek, ne toho, jaký druh věci to je.
use NyonCode\WireCore\Foundation\Routing\Contracts\ProvidesPages;use NyonCode\WireCore\Foundation\Routing\RoutePage; public static function pages(): array { return [ 'index' => ListOrders::class, 'create' => CreateOrder::class, 'view' => ViewOrder::class, 'edit' => RoutePage::make(EditOrder::class)->permission('orders.update'), ];}
a aplikace je zaregistruje uvnitř své vlastní skupiny:
// routes/web.phpRoute::prefix('admin') ->middleware(['auth', 'verified']) ->domain(config('app.admin_domain')) ->group(function () { Route::wireResources(); Route::wireResource(OrderResource::class); // nebo po jednom });
Prefix, middleware i doména jsou vaše — jsou to obyčejné Laravelí routy registrované ve skupině, ve které jste macro zavolali. Resource, který nedeklaruje stránky, se přeskočí; tak zůstane interní nebo vnořený resource neroutovaný. Jmenovat takový resource explicitně naopak vyhodí výjimku, protože to je chyba, ne volba.
Tvar URL
| Druh stránky | URL | Jméno routy |
|---|---|---|
index |
{prefix} |
wire.{key}.index |
create |
{prefix}/create |
wire.{key}.create |
view |
{prefix}/{record} |
wire.{key}.view |
edit |
{prefix}/{record}/edit |
wire.{key}.edit |
| cokoli dalšího | {prefix}/{druh} |
wire.{key}.{druh} |
{prefix} je registrovaný klíč, takže klíč v menu a URL se shodují, aniž by se
kterýkoli z nich opakoval. {record} je klíč, ne navázaný model: stránky si
záznam resolvují samy, což nechává soft-delete scope, tenant guard i
non-Eloquent zdroj rozhodnutím stránky, ne routeru.
Druh stránky, který router nezná, sedí na vlastním jméně a může říct jinak přes
uri(). Ta URI zároveň rozhoduje, jestli je stránka o jednom záznamu:
{record} v ní znamená, že ho routa bere — a že je stránka záložkou v
sub-navigaci záznamu, ať se jmenuje jakkoli.
'history' => RoutePage::make(OrderHistory::class)->uri('{record}/history'), // stránka záznamu'archive' => RoutePage::make(ArchivedOrders::class), // stránka seznamu
Stránka nikdy cestu nezabere a nikdy o ni nepřijde. Laravel klíčuje routy
podle metody a URI, takže druhá routa na stejné cestě tu první nezastíní —
nahradí ji i se jménem, a beze slova. Na stránku míří její položka menu, takže to
nesmí udělat ani jedna strana: wireResources() odmítne stránku, jejíž cestu už
nějaká routa obsluhuje, a routu zaregistrovanou přes stránku níž v route souboru
odmítne, jakmile jsou načtené všechny routy — při bootu, a tedy i při
route:cache, ne až při prvním kliknutí. Stejná stránka zaroutovaná dvakrát přes
sebe nic nemění a projde.
Vnořený resource sedí pod jedním záznamem
rodiče — {prefix rodiče}/{parent}/{jeho prefix} — a dědí middleware a doménu
rodiče, takže co chrání objednávku, chrání i její řádky:
| Druh stránky | URL | Jméno routy |
|---|---|---|
index |
orders/{parent}/order-lines |
wire.order-lines.index |
edit |
orders/{parent}/order-lines/{record}/edit |
wire.order-lines.edit |
Oprávnění, middleware a domény
RoutePage::permission() dosedne na routu jako Laravelí can: middleware. Nic
tady autorizaci neimplementuje znovu — odpovídá na ni Gate, přesně jako
u akcí, sloupců a widgetů, takže spatie/laravel-permission
i nyoncode/laravel-permission-extended fungují beze změny. Odmítnutí se stane
v routeru, dřív než se stránka vykreslí nebo padne dotaz.
Na úrovni resource přidává ConfiguresRoutes tři věci, které patří
jednomu resource a ne celé skupině:
public static function routeMiddleware(): array { return ['can:tenants.view']; }public static function routeDomain(): ?string { return '{tenant}.example.com'; }public static function routePrefix(): ?string { return 'billing/tenants'; }
Parametr domény se dostane do vašeho TenantResolveru jako každý jiný parametr
routy. Samotná tenancy zůstává, kde je — globální scope nad každým dotazem, ne
záležitost routování; viz Autorizace.
Zóny
Víc mount pointů nad jednou sadou resources — admin, business, production.
Resource může být v jedné z nich, ve víc, nebo ve všech: zóna násobí, kde je
stránka dosažitelná, ne kolikrát je zaregistrovaná.
Zóna je jméno route skupiny a nic víc:
Route::name('admin.')->prefix('admin')->middleware(['web','auth','can:admin']) ->group(fn () => Route::wireResources()); Route::name('business.')->prefix('business')->middleware(['web','auth','can:business']) ->group(fn () => Route::wireResources(only: ['orders']));
admin.wire.orders.index → admin/ordersbusiness.wire.orders.index → business/orders
Rozděluje je to volání name(). Vynech ho na druhé skupině a obě zóny
zaregistrují wire.orders.index, kde pozdější tiše vyhraje každý lookup — proto
je cesta přes config níž bezpečnější
způsob, jak zóny deklarovat: tam je zóna klíčem pole a zapomenout se nedá.
Které resources zóna obsahuje, říká only / except a nic jiného — žádný druhý
seznam, který by se musel držet v souladu s routami.
Odkazování uvnitř zóny. Každá otázka na URL zní „kde je tenhle klíč v téhle zóně“, takže zóna cestuje s ní:
ResourceRoutes::urlFor('orders', zone: 'business'); // /business/ordersResourceRoutes::urls(zone: 'business'); // jen to, co business routujeapp(Workspace::class)->navigation(zone: 'business'); // položky odkazující do business
Klíč, který zóna neroutuje, odpoví null a vykreslí se bez odkazu — přesně jako
neroutovaný resource. Když má menu obsahovat jen to, na co tahle zóna opravdu
dosáhne, řekni si o to:
app(Workspace::class)->navigation(zone: 'business', linkedOnly: true);
Volitelné, ne pravidlo, protože důvody, proč položka nemá URL, jsou dva různé
a Workspace je nerozliší: jedna může být routovaná v jiné zóně, druhá nikde.
A shell s vlastním URL schématem má tady bez odkazu úplně všechno a stejně chce
všechny položky — je to volající, kdo ví, ve kterém případě je. Skupina, které
vypadnou všechny položky, zmizí celá místo prázdného nadpisu.
Vlastní adresa zóny. /business samo odpoví vždycky. Pokud si ji nic
nenárokuje, wireResources() tam dá vstup — wire.home, v zóně
business.wire.home — který člověka pošle na první stránku skupiny, kterou
opravdu smí otevřít: první v pořadí sidebaru (skupiny podle svého řazení, pak
položky podle svého, skryté vynechané) a vynechá stránku, jejíž middleware can:
by Gate odmítl, protože přesměrování do 403 je horší přistání než žádné. Nic
k otevření je 403; nic zaregistrovaného je 404. Na tuhle adresu také
wire:install nasměruje home Fortify, takže přihlášení skončí v adminu. Routa,
kterou má aplikace na té cestě, vyhraje, ať je před skupinou, nebo za ní: vstup
je pohodlí a ustoupí.
Landing page zóny. Nárokovat si adresu místo toho je jedna metoda: prázdný
prefix nepřidá segment, takže index té stránky sedne na vlastní cestu skupiny
a vstup výše se pak neregistruje:
final class BusinessOverview extends Dashboard implements ConfiguresRoutes, ProvidesPages{ public static function pages(): array { return ['index' => ShowBusinessOverview::class]; } public static function routePrefix(): ?string { return self::ROOT; } }
business.wire.business-overview.index → businessbusiness.wire.orders.index → business/orders
Která zóna přistane kde, říká only / except — jako každá jiná otázka na
členství: dejte každé zóně vlastní dashboard a vypište ho tam. Dvě stránky, které si
nárokují kořen jedné skupiny, jsou odmítnuty — Laravel klíčuje routy podle
URI, takže by druhá tu první nahradila i se jménem routy a zůstala by položka
menu, která vypadá zaroutovaně a tiše nikam neodkazuje.
Zóna, která chce pevný cíl místo první stránky, napíše obyčejný redirect vedle skupiny — před ni, nebo za ni; vstup ho pak nechá být:
Route::redirect('business', 'business/orders');
Nad zónami: kam vede přihlášení. Při více zónách nepatří / žádné z nich
a právě tam pošle Fortify home člověka po přihlášení.
Route::wireZoneEntry() ji routuje jako wire.zones a zónu určí bez ptaní
pokaždé, když existuje jediná odpověď, v tomto pořadí:
- vlastní volba člověka — uživatelský model s
HasPreferredZone, wire-panels.routes.zone_entry.primary,- jediná zóna, do které člověk smí.
Krok se počítá jen tehdy, když člověk do jmenované zóny smí — zeptá se
middlewaru can: té zóny, stejně jako vstup výše — takže zastaralá preference
nebo primární zóna za oprávněním, které nemá, propadne dál místo 403. Výběr zón
se ukáže jen při skutečné volbě; bez nastaveného view vezme první zónu, do které
smí.
Route::middleware(['web', 'auth'])->group(fn () => Route::wireZoneEntry('/')); // config/wire-panels.php'routes' => [ 'zone_entry' => [ 'uri' => null, // nebo '/' — tatáž routa, deklarovaná z configu 'primary' => 'business', 'view' => 'zones', // dostane $zones (klíč => URL) a $primary ],], // config/fortify.php'home' => '/',
use NyonCode\WirePanels\Contracts\HasPreferredZone; class User extends Authenticatable implements HasPreferredZone{ public function preferredZone(): ?string { return $this->preferred_zone; }}
Znovu vybrat jde jedním odkazem: ?choose přeskočí přesměrování a ukáže
výběr komukoli, s primární zónou i bez ní — kam míří položka „všechny zóny"
v přepínači zón, route('wire.zones', ['choose' => 1]). Zóny se čtou z routeru,
ne z configu, takže zóny deklarované v route souboru se počítají stejně jako
záznamy wire-core.routes.groups; ZoneDirectory::reachableBy($user) je týž seznam, jaký kreslí
přepínač.
Odkud se zóna bere. Zone::current() ji přečte z routy, která se právě
vykresluje. Na stránce, kterou zaregistrovalo Route::wireResources(), to platí
i při Livewire round tripu: každá taková routa nese RememberPage, který Livewire
spustí znovu jako persistent middleware nad původním requestem stránky, takže
Zone::current(), currentKey() a currentPage() odpovídají za stránku při
druhém vykreslení stejně jako při prvním. Kdekoli jinde — komponenta na vaší
vlastní routě, partial layoutu na ručně psané routě — je to volání pro plný
render stránky:
public ?string $zone = null; public function mount(): void{ $this->zone = Zone::current();}
Tam Route::currentRouteName() během Livewire round tripu odpoví livewire.update,
takže komponenta, která se zeptá znovu uprostřed updatu, nedostane nic — a paleta,
která hledá při každém stisku klávesy, by odkazovala mimo svoji zónu a přitom
vypadala bezvadně. Přečtěte to jednou, uložte do public property a nechte to Livewire
přenášet. Command paleta to přesně tak dělá, takže paleta v zónovaném layoutu
nepotřebuje žádnou konfiguraci.
Popis skupin v configu
Stránky panelu jsou jedna skupina rout, panel, a routy každého balíčku se
umisťují stejně — z route souboru, nebo z configu, stejným kódem.
V route souboru, uvnitř libovolné skupiny, kterou kolem umí postavit Laravel:
// routes/web.phpRoute::middleware(['web', 'auth', 'can:admin'])->prefix('admin') ->group(fn () => Route::wire('panel', zone: 'admin'));
zone: je prefix jména routy, kterým zóna je, napsaný za vás. Route::wireResources()
je totéž volání bez něj — Route::wire('panel', only: [...], except: [...]).
V configu tytéž skupiny popsané místo vypsané, které zaregistruje jediný route soubor frameworku:
// config/wire-core.php'routes' => [ 'defaults' => [], // pod každým záznamem 'groups' => [ 'admin' => ['uses' => 'panel', 'prefix' => 'admin', 'middleware' => ['web', 'auth', 'can:admin']], 'business' => ['uses' => 'panel', 'prefix' => 'business', 'only' => ['orders', 'customers']], 'zones' => ['uri' => '/'], // adresa nad nimi ], ],
Klíč záznamu je skupina, kterou umisťuje, pokud uses nejmenuje jinou; pak je
klíč zóna, což je důvod dát tomuhle přednost před ručně psanými skupinami:
v route souboru je ->name('business.') řádek, který se dá vynechat, a vynechání
znamená, že jedna zóna tiše převezme odkazy druhé. Klíč pole vynechat nejde
a opakovat se nemůže.
| Klíč | Co to je |
|---|---|
prefix, domain, middleware, as, where, namespace, scope_bindings |
Atributy skupiny Laravelu, jak je bere Route::group() |
without_middleware |
excluded_middleware Laravelu |
can |
Přidá skupině can:… |
zone, tenant, only, except |
Vlastní volby panelu — tenant je Tenancy |
routes |
Změny jednotlivých rout podle klíče: ['orders' => ['can' => 'orders.export']] — middleware, without_middleware, can, where |
uses, enabled |
Skupina, kterou záznam umisťuje; false ho přeskočí, stejně jako 'enabled' => false |
Záznam, který nejmenuje žádný middleware, je hlídaný. Panel má jako vlastní
výchozí bod ['web', 'auth'] — defaults jde přes něj, záznam přes obojí —,
protože registruje obrazovky pro vytvoření, úpravu a smazání a skupina bez auth
je naservíruje komukoli s URL, aniž by cokoli vypadalo špatně.
Pasti. Skupiny z configu se registrují dřív, než se přečte routes/web.php,
takže se matchují před vším v něm: aplikace s catch-all routou pod stejným
prefixem tu skupinu umístí do svého route souboru a pořadí rozhodne sama. Tatáž
stránka na dvou adresách pod jedním jménem routy — config i route soubor umisťují
nepojmenovaný panel — se odmítne, ne smíří; umístit ji přes sebe samu nic nemění
a je dovolené. A stránka, která existuje jen uvnitř firmy, se ve skupině bez
{tenant} přeskočí, takže aplikace bez tenancy nikdy nedostane adresu, která by
mohla odpovědět jen 404.
Jak na ně odkazovat
URL už nemusí nikdo psát ručně. Z kódu se zeptejte třídy:
OrderResource::url(); // /admin/ordersOrderResource::url('edit', $order); // /admin/orders/7/editOrderLineResource::url('index', parameters: ['parent' => $order]); // /admin/orders/7/order-linesTaskBoard::url(parameters: ['week' => 12]); // /admin/board?week=12OrderResource::url(zone: 'business'); // /business/orders
url() přichází s DescribesRecords, který resource už používá, a s
Pages\Page; jakákoli jiná třída, která zná svůj klíč
— dashboard —, si ho přidá přes use InteractsWithPageUrls;. Je to volání přes
klíč níže s vyplněným klíčem samotné třídy, takže odpovídá přesně tím, čím ono,
včetně null, a přejmenovaný klíč nemůže nechat odkaz mířit na ten starý.
O tom, co sestaví, rozhodují tři věci:
- Model se převede na svůj klíč, záznam i jakýkoli parametr.
{record}a{parent}jsou klíče, které si stránka dohledá sama, a model, jehož route key je slug, by jinak dal URL, na kterém stránka nic nenajde. - Parametr, který routa nejmenuje, se stane query stringem — takhle si vlastní stránka bere své volby.
- Zóna je ve výchozím stavu ta vykreslovaná, přečtená ze jména routy — i při
Livewire round tripu, pro každou stránku zaregistrovanou přes
Route::wireResources()(viz odkud se zóna bere). Komponenta na vaší vlastní routě si zónu přečtenou v mountu nechá a předá ji —url(zone: $this->zone).parentvnořeného resource se nehádá: předejte ho.
Položka menu už nese URL stránky svého klíče a výsledek hledání nese URL svého záznamu:
$item->getUrl(); // /admin/orders — doplní Workspace, null když neroutováno$result->url; // /admin/orders/7 — z klíče a klíče záznamu
Obojí přichází z ResolvesPageUrls, na které odpovídá wire-panels a na které
wire-core odpovídá null, když routing nevlastní žádný balíček. null je
plnohodnotná odpověď: položka menu bez href se vykreslí a resource, který
nedeklaruje stránky, je neodkazovaný záměrně. Položka nebo výsledek, který si URL
pojmenuje sám, vždy vyhraje — externí odkaz nebo aplikace s vlastním URL schématem
shellu.
Kód, který drží klíč místo třídy, dojde ke stejné odpovědi napřímo:
ResourceRoutes::urlFor('orders'); // /admin/ordersResourceRoutes::urlFor('orders', 'edit', ['record' => 7]); // /admin/orders/7/editResourceRoutes::urls(); // ['orders' => '/admin/orders', …]
Full-page Livewire komponenta potřebuje layout a framework ho nedodává — nastavte
si livewire.component_layout na svůj vlastní.
Routing API
OrderResource::url(string $page = 'index', Model|int|string|null $record = null, array $parameters = [], ?string $zone = null): ?string // každá třída s InteractsWithPageUrlsResourceRoutes::all(array $only = [], array $except = []): array // každý klíč, který deklaruje, navíc `wire.home` v kořeniResourceRoutes::for(string $class, array $pages = []): array // jeden, nebo vyhodí výjimku; `$pages` routuje jen tyResourceRoutes::urlFor(string $key, string $page = 'index', array $parameters = [], ?string $zone = null): ?stringResourceRoutes::urls(string $page = 'index', ?string $zone = null): arrayResourceRoutes::uriFor(string $name, string|RoutePage $page): string // the segment it sits atResourceRoutes::takesRecord(string $name, string|RoutePage $page): bool // read off that URI, not off the kindResourceRoutes::zoneEntry(string $uri = '/'): Route // `wire.zones`; the macro is Route::wireZoneEntry()Route::wire(string $group, mixed ...$options): array // any package's route group, keyed; `panel` takes zone:, tenant:, only:, except: ZoneDirectory::all(): array // zone => its shortest route, in registration orderZoneDirectory::reachableBy(?Authenticatable $user): array // zone => URL, only those its `can:` lets inZoneDirectory::landingFor(?Authenticatable $user): ?string // preference, primary, the only one — or null
A dvě čtení jména routy stránky, která každý volající dostane z jednoho
ukotveného vzoru — livewire.update obsahuje wire., takže hledání podřetězce
hlásí zónu přesně na tom požadavku, který žádnou nemá:
Zone::current(): ?string // zóna právě vykreslované stránky — i při jejích round tripech, u stránky z wireResourcesZone::of(?string $routeName): ?stringZone::currentPage(): ?string // druh té stránky — `index`, `view`, `edit`, nebo vlastní druh resourceZone::pageOf(?string $routeName): ?stringZone::prefix(?string $zone): string
urlFor() odpoví null ve dvou případech: když klíč nic neroutuje, a když routa
potřebuje parametr, který tohle volání nedalo — třeba resource na doméně
{tenant}. Obojí se vykreslí jako „bez odkazu“, místo aby to shodilo menu.
Z wire-core na to sáhni přes ResolvesPageUrls, na které odpovídá wire-panels
a které odpoví null, když routing nevlastní žádný balíček.
AuthorizesUrls je otázka po „kde": smí tento člověk tu URL otevřít.
wire-panels na ni odpovídá RouteAccess, který položí can: middlewaru routy
(permission() stránky, skupinu zóny) Gate. Bez balíčku, který routuje, je
odpověď ano. Průvodce se na ni ptá, než
někoho dovede na jinou stránku, takže krok nikdy neskončí na 403.
app(AuthorizesUrls::class)->allowsUrl(string $url, ?Authenticatable $user): bool
Související
- Stránky — komponenty, na které tyhle routy vedou
- Navigace — odkud se bere URL položky menu
- Autorizace — gates, na které se ptá
permission()acan: - Konfigurace — celý blok
wire-panels.routes - Admin shell — layout pro stránky, na které tyhle routy vedou