⌘K

Panels

Resources

Jedna entita svázaná s povrchy, které vystavuje — kontrakty, které implementuje, jak se jmenuje, jak se registruje a na co registr odpoví, aniž by kterýkoli z nich postavil.

Na této stránce

Resource váže jednu entitu na povrchy, které vystavuje — její seznam, formulář a read-only pohled — takže žijí v jedné deklaraci místo ručního drátování do každé Livewire komponenty, která je zrovna zobrazuje.

Jak to funguje

Tabulka, formulář a infolist jsou samostatné primitivy a zůstávají jimi: resource nemění nic na tom, jak fungují. Co přidává, je vlastník nad nimi — jedna třída, která odpovídá „tohle je entita Order, takhle se vypisuje, takhle se edituje“ — a registr, který umí odpovědět „které resources existují“ a „který vlastní App\Models\Order“, aniž by kterýkoli z těch povrchů postavil.

To rozdělení je důvod, proč je resource několik malých kontraktů místo jednoho velkého:

Kontrakt Odpovídá na
DescribesResource co je entita zač: klíč, model, jednotné a množné označení
ProvidesResourceTable jak se vypisuje
ProvidesResourceForm jak se zakládá a edituje
ProvidesResourceInfolist jak se jeden záznam ukáže read-only

Resource implementuje ty, které má. Read-only audit log implementuje identitu a tabulku a nic dalšího, a stránka, která potřebuje formulář, ho omylem nedostane — typ to řekne.

DescribesResource je statický a povrchy jsou instanční metody, a ten důvod je mechanický, ne stylový. Menu se ptá na popisek a registr směruje model na jeho vlastníka dřív, než se cokoli instancovalo; metadata tedy nesmí instanci vyžadovat. Povrchy naopak skládají builder, který vlastní a na hostitele už napojil volající — přesně jak to dělá RelationManager i každá WithTable komponenta — takže ty instanci dostanou a vrátí.

Základní použití

use App\Models\Order;
use NyonCode\WireTable\Columns\TextColumn;
use NyonCode\WireCore\Core\Resources\Concerns\DescribesRecords;
use NyonCode\WireCore\Core\Resources\Contracts\DescribesResource;
use NyonCode\WirePanels\Resources\Contracts\ProvidesResourceTable;
use NyonCode\WireTable\Table;
 
final class OrderResource implements DescribesResource, ProvidesResourceTable
{
use DescribesRecords;
 
public static function modelClass(): ?string
{
return Order::class;
}
 
public function table(Table $table): Table
{
return $table->columns([
TextColumn::make('number'),
TextColumn::make('customer.name'),
]);
}
}

DescribesRecords dodá zbylé tři odpovědi z třídy modelu, takže deklarace výš je kompletní: klíč orders, popisek Order, množné Orders.

Pojmenování

Klíč není kosmetika. Je to konfigurační rukojeť, introspekční jméno a segment routy, který stránka použije, takže musí přežít změnu popisku i přesun namespace — proto se odvozuje od modelu, ne od třídy resource ani od popisku:

App\Models\OrderLine → key 'order-lines' · label 'Order Line' · plural 'Order Lines'
App\Models\Person → key 'people' · label 'Person' · plural 'People'

Množné číslo dělá Laravelův inflector, takže nepravidelná slova jsou správně bez vypisování. Přebijte jen tu odpověď, která je špatně:

public static function pluralLabel(): string
{
return 'Line items';
}

Resource bez modelu — postavený nad DataSource místo Eloquentu — vrací null z modelClass() a jména si odvodí z vlastního názvu třídy, s useknutým koncovým Resource. Registruje se a vypisuje jako každý jiný; jen ho nejde najít podle modelu.

Registrace

Resources se deklarují v konfiguraci:

// config/wire-core.php
'resources' => [
App\Resources\OrderResource::class,
App\Resources\CustomerResource::class,
],

Je to registr, ne panel: drží názvy tříd a odpovídá na dvě otázky o nich. Nevlastní routing, URL shell ani navigační strom.

Přidat jeden za běhu — což je přesně to, co by dělal scanner s atributy — znamená vytáhnout registr a zaregistrovat přímo:

use NyonCode\WireCore\Core\Resources\ResourceRegistry;
 
app(ResourceRegistry::class)->register(OrderResource::class);

Zaregistrovat tutéž třídu dvakrát je no-op, protože to dělá jak slučování konfigurace, tak provider nabootovaný dvakrát. Dvě různé třídy hlásící se k jednomu klíči naopak vyhodí výjimku: ta druhá by tiše převzala routing té první.

Jak je najít samy

Složku resource jde zaregistrovat jako složku:

// config/wire-core.php
'discover' => [
'resources' => ['App\\Resources' => app_path('Resources')],
'dashboards' => ['App\\Dashboards' => app_path('Dashboards')],
],

Namespace na adresář, tak jak je mapuje PSR-4. Při bootu se zaregistruje každá konkrétní třída v adresáři, která implementuje DescribesResource (u dashboards dědí z Dashboard) — až po vyjmenovaných, takže třída vyjmenovaná i nalezená se zaregistruje jednou a pořadí seznamu vyhrává. Cokoli dalšího ve složce — abstraktní základ, trait, pomocník — se přeskočí, protože složka resource smí obsahovat to, z čeho se skládají. Třída, která se nenačte autoloaderem, se nenajde: discovery jde přes autoloader, takže vidí to, co aplikace umí načíst.

Při každém bootu je to jeden výpis adresáře a jedno class_exists() na soubor, a proto je to vypnuté, dokud se nějaký adresář nepojmenuje.

Čtení registru

$registry = app(ResourceRegistry::class);
 
$registry->all(); // ['orders' => OrderResource::class, …]
$registry->find('orders'); // OrderResource::class | null
$registry->has('orders'); // bool
$registry->forModel(Order::class); // OrderResource::class | null

Každá z těch odpovědí vzniká jen ze statického kontraktu, takže sestavení menu z all() nikdy neskládá tabulku.

Rozšířený příklad

Resource objednávky se všemi třemi povrchy — seznam, který stránka vykreslí, formulář sdílený zakládáním i editací, a read-only pohled:

use App\Models\Order;
use NyonCode\WireCore\Infolists\Infolist;
use NyonCode\WireCore\Infolists\Components\TextEntry;
use NyonCode\WireForms\Components\Select;
use NyonCode\WireForms\Components\TextInput;
use NyonCode\WireForms\Forms\Form;
use NyonCode\WireTable\Columns\MoneyColumn;
use NyonCode\WireTable\Columns\TextColumn;
use NyonCode\WireCore\Core\Resources\Concerns\DescribesRecords;
use NyonCode\WireCore\Core\Resources\Contracts\DescribesResource;
use NyonCode\WireForms\Contracts\ProvidesResourceForm;
use NyonCode\WireCore\Infolists\Contracts\ProvidesResourceInfolist;
use NyonCode\WirePanels\Resources\Contracts\ProvidesResourceTable;
use NyonCode\WireTable\Table;
 
final class OrderResource implements
DescribesResource,
ProvidesResourceTable,
ProvidesResourceForm,
ProvidesResourceInfolist
{
use DescribesRecords;
 
public static function modelClass(): ?string
{
return Order::class;
}
 
public function table(Table $table): Table
{
return $table
->columns([
TextColumn::make('number')->searchable(),
TextColumn::make('customer.name')->label('Customer'),
MoneyColumn::make('total', 'Kč'),
])
->defaultSort('number', 'desc');
}
 
public function form(Form $form): Form
{
return $form->schema([
TextInput::make('number')->required(),
Select::make('customer_id')
->relationship('customer', 'name')
->required(),
]);
}
 
public function infolist(Infolist $infolist): Infolist
{
return $infolist->schema([
TextEntry::make('number'),
TextEntry::make('customer.name')->label('Customer'),
TextEntry::make('total')->money('Kč'),
]);
}
}

Jeden form() slouží zakládání i editaci záměrně — rozejít se formulář pro založení a pro editaci je přesně ta chyba, které tenhle tvar předchází. Kde se opravdu lišit musí, předá stránka formulář, který resource teprve tvaruje, místo aby resource deklaroval dva.

Perzistence zůstává formuláři: Form už vlastní životní cyklus ukládání a resource nad non-Eloquent zdrojem zapisuje přes Form::using().

Vnořené resource

Některé záznamy dávají smysl jen uvnitř jiného — řádky objednávky, úkoly projektu. Resource to řekne tím, že pojmenuje rodiče a relaci rodičovského modelu, která ho drží:

use NyonCode\WirePanels\Resources\Contracts\NestedResource;
 
final class OrderLineResource implements DescribesResource, NestedResource, ProvidesPages, ProvidesResourceForm, ProvidesResourceTable
{
use DescribesRecords;
 
public static function parentResource(): string
{
return OrderResource::class;
}
 
public static function parentRelationship(): string
{
return 'lines'; // Order::lines(), hasMany
}
 
public static function pages(): array
{
return ['index' => ListOrderLines::class, 'create' => CreateOrderLine::class, 'edit' => EditOrderLine::class];
}
 
// table(), form() — jako u každého resource
}

Všechno jde přes rodičovský záznam. Stránky jsou routované pod ním — orders/{parent}/order-lines, …/create, …/{record}/edit — a klíč {parent} čtou z routy. Seznam ukáže jen řádky té objednávky; stránka záznamu hledá svůj řádek přes $order->lines(), takže klíč řádku jiné objednávky je 404, ne cizí řádek; a stránka založení váže formulář na $order->lines()->make(), takže nový řádek se založí pod objednávkou s už nastaveným klíčem. URL stránek nesou rodiče, takže Nový, přesměrování po uložení i záložky záznamu zůstanou uvnitř.

Drobečková navigace a záložky následují. Stránka řádku vede Orders › ORD-17 › Order lines › Edit Order line, rodičovský záznam pojmenovaný podle name, title, number, label nebo subject, jinak podle štítku a klíče. A stránky rodičovského záznamu dostanou záložku na seznam — rodič pro to nic nedeklaruje, stačí, že ho vnořený resource jmenuje, a záložka chybí tomu, koho by routa seznamu odmítla.

Meze dává URL. Routa nese jeden {parent}, takže rodič vnořeného resource sám vnořený není — to se odmítne při registraci rout. Založení potřebuje relaci, která umí vyrobit potomka znalého rodiče — hasMany, morphMany a jejich jedna-k-jedné příbuzné; belongsToMany odmítne se zprávou, která jmenuje cestu ven. Vnořený resource bez rodiče nemá URL, a tedy ani místo v menu: nechte ho bez ProvidesNavigation.

Introspekce

describe-resource hlásí, co resources aplikace deklarují — identitu, které povrchy mají a navigační položku:

describe-resource # všechny registrované resources
describe-resource orders # jeden, podle klíče
describe-resource App\Resources\OrderResource # nebo podle třídy

Povrchy se hlásí jako deklarované / nedeklarované, ne svým obsahem: složit je by stálo přesně to, čemu se statická půlka vyhýbá, a describe-table a describe-form na to už odpovídají za stránky, které je vykreslují.

DescribesResource API

Metoda Vrací Účel
static key(): string string Stabilní identifikátor, unikátní v rámci registru
static modelClass(): ?string class-string|null Vlastněný Eloquent model, nebo null u non-Eloquent zdroje
static label(): string string Jednotné označení
static pluralLabel(): string string Množné označení

API povrchových kontraktů

Kontrakt Metoda Veze balíček
ProvidesResourceTable table(Table $table): Table wire-panels
ProvidesResourceForm form(Form $form): Form wire-forms
ProvidesResourceInfolist infolist(Infolist $infolist): Infolist wire-core
ProvidesRelationManagers relationManagers(): array wire-panels
ManagesTrashedRecords (značka) wire-panels
NestedResource static parentResource(): string, static parentRelationship(): string wire-panels
ProvidesNavigation static navigation(): NavigationItem wire-core
ProvidesBreadcrumbs breadcrumbs(): array wire-core
ProvidesPages static pages(): array wire-core
ConfiguresRoutes static routeMiddleware(): array, static routeDomain(): ?string, static routePrefix(): ?string wire-core
GloballySearchable static globallySearchableAttributes(): array, static toGlobalSearchResult(object): GlobalSearchResult wire-core

ResourceRegistry API

Metoda Vrací Účel
register(string $resource): void void Přidá třídu resource; vyhodí výjimku, když to resource není nebo když je klíč zabraný jinou třídou
registerMany(mixed $resources): void void Zaregistruje, co bylo v configu. Chybějící nebo poškozená položka se přeskočí, ne fatal — publikovaný config se zbloudilou hodnotou nesmí položit aplikaci při bootu
all(): array array<string, class-string> Všechny registrované resources, klíčované klíčem
registeredClasses(): array array<string, class-string> Slib RegistrySource, ze kterého čte menu, router i paleta. Táž odpověď jako all(), schválně jiná metoda, aby all() mohla nabírat další konzumenty, aniž by se tím slibem stala
find(string $key): ?string class-string|null Resource s tímhle klíčem
has(string $key): bool bool Jestli je klíč registrovaný
forModel(string $model): ?string class-string|null Resource vlastnící danou třídu modelu

Související