⌘K

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.php
Route::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/orders
business.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/orders
ResourceRoutes::urls(zone: 'business'); // jen to, co business routuje
app(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 → business
business.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í:

  1. vlastní volba člověka — uživatelský model s HasPreferredZone,
  2. wire-panels.routes.zone_entry.primary,
  3. 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.php
Route::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/orders
OrderResource::url('edit', $order); // /admin/orders/7/edit
OrderLineResource::url('index', parameters: ['parent' => $order]); // /admin/orders/7/order-lines
TaskBoard::url(parameters: ['week' => 12]); // /admin/board?week=12
OrderResource::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). parent vnoř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/orders
ResourceRoutes::urlFor('orders', 'edit', ['record' => 7]); // /admin/orders/7/edit
ResourceRoutes::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 InteractsWithPageUrls
ResourceRoutes::all(array $only = [], array $except = []): array // každý klíč, který deklaruje, navíc `wire.home` v kořeni
ResourceRoutes::for(string $class, array $pages = []): array // jeden, nebo vyhodí výjimku; `$pages` routuje jen ty
ResourceRoutes::urlFor(string $key, string $page = 'index', array $parameters = [], ?string $zone = null): ?string
ResourceRoutes::urls(string $page = 'index', ?string $zone = null): array
ResourceRoutes::uriFor(string $name, string|RoutePage $page): string // the segment it sits at
ResourceRoutes::takesRecord(string $name, string|RoutePage $page): bool // read off that URI, not off the kind
ResourceRoutes::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 order
ZoneDirectory::reachableBy(?Authenticatable $user): array // zone => URL, only those its `can:` lets in
ZoneDirectory::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 wireResources
Zone::of(?string $routeName): ?string
Zone::currentPage(): ?string // druh té stránky — `index`, `view`, `edit`, nebo vlastní druh resource
Zone::pageOf(?string $routeName): ?string
Zone::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() a can:
  • Konfigurace — celý blok wire-panels.routes
  • Admin shell — layout pro stránky, na které tyhle routy vedou