Tabulka
Zdroje dat
Odkud tabulka bere řádky, když nejsou v databázi — kontrakt zdroje a co od něj tabulka pořád očekává.
Na této stránce
Tabulka čte řádky přes zdroj dat. Ve výchozím stavu ten zdroj obaluje
Eloquent builder a nikdy ho neuvidíte — ->model() a ->query() fungují přesně
jako dosud. Vlastní zdroj deklarujete, když řádky nejsou v databázi: read model,
seznam DTO, odpověď API.
Mechanismus
Čtení tabulky jsou tři otázky a zdroj odpovídá na všechny: které řádky
odpovídají (count), tahle jejich stránka (paginate) a řádek za tímhle
klíčem (resolveRecord). Zdroj dostane QueryPlan — hledání, filtry, řazení
a joiny, které tabulka vyřešila ze stavu — a PagingRequest pro výřez.
Plán je deklarativní. Filtr je sloupec, operátor a hodnota, nikdy closure, a
právě to dělá zdroj mimo databázi vůbec možným: umí plán přečíst a odpovědět na
něj metodami Collection místo SQL.
Na co zdroj odpovědět neumí, to deklaruje, že neumí — a engine pak vyhodí výjimku místo toho, aby vrátil řádky, které tiše ignorovaly půlku dotazu:
public function capabilities(): CapabilitySet{ return new CapabilitySet( Capability::Filterable, Capability::Sortable, Capability::Paginable, ); // žádné SqlExpression, žádné Joinable — dotaz na ně vyhodí výjimku}
To je celá bezpečnostní vlastnost. Tabulka nad API, které neumí řadit, to musí říct — tabulka, která řadí podle ničeho a vypadá seřazeně, je horší než ta, která zahlásí chybu.
Tabulka nad řádky v paměti
use NyonCode\WireTable\Data\CollectionDataSource; public function table(Table $table): Table{ return $table ->dataSource(new CollectionDataSource([ ['id' => 1, 'name' => 'Ada', 'score' => 90], ['id' => 2, 'name' => 'Grace', 'score' => 70], ])) ->columns([ TextColumn::make('name')->searchable()->sortable(), TextColumn::make('score')->sortable(), ]);}
Žádné ->model(), žádné ->query(). Hledání, filtry, řazení i stránkování
fungují; zdroj na ně odpoví nad polem — text řadí podle jazyka aplikace (přes
intl Collator, kde je nainstalovaný), takže Černý je před Dvořákem, ne za
Veselým. Totéž platí o částech postavených nad nimi:
souhrny v patičce sčítají vše, co filtry propustí, ne jen stránku, a „vybrat vše
odpovídající" předá hromadné akci každý odpovídající řádek.
Každý řádek dorazí do sloupců, akcí i hromadných akcí jako CollectionRow —
model jen pro čtení s klíčem, jaký mu dal zdroj, takže $record->name
a $record->getKey() se čtou jako u Eloquentu. save() nebo delete() na něm
vyhodí výjimku: nemá tabulku, do které by zapsal.
Co tabulka nad kolekcí neumí
CollectionDataSource deklaruje čtyři schopnosti a zbytek odmítá. Každé
odmítnutí vyhodí UnsupportedQueryAspectException se jménem aspektu:
| Když chcete | Odkud to přijde | Výsledek |
|---|---|---|
| Raw SQL výraz | Column::sortUsing() se SQL, ->sqlExpression() |
vyhodí [sql_expression] |
| Cestu přes relaci | TextColumn::make('company.name') |
vyhodí [joinable] |
| Agregace přes subquery | rollupy ->sums(), ->counts() |
vyhodí [aggregateable] |
| Cursor stránkování | ->paginationMode('cursor') |
vyhodí [cursor paging] |
| Detekci změny pro polling | ->poll() |
vrátí null; polling místo toho porovná řádky |
Sama tabulka odmítá to, co umí jen Eloquent builder. Deklarovat to nic nestojí;
ve chvíli, kdy se to použije, to CustomDataSourceException pojmenuje, místo
aby spadla na chybějícím modelu:
| Když chcete | Odkud to přijde | Výsledek |
|---|---|---|
| Filtr, který se aplikuje jako dotaz | DateFilter, Filter::query(fn ($query) => …) |
vyhodí, jakmile dostane hodnotu |
| Hledání nebo řazení zapsané jako callback | ->searchable(true, fn ($query, $term) => …), ->sortable(true, fn …) |
vyhodí při hledání či řazení |
| Výběr jako builder | selectedRecordsQuery() |
vyhodí; getSelectedRecords() místo toho čte řádky |
| Zápis řádku zpět | $record->save(), $record->delete() |
vyhodí |
Nic z toho nejsou omezení, která by se dala tiše obejít. Tabulka nad seznamem v paměti je omezená tabulka a to omezení je vidět hned při první žádosti o něco mimo něj.
Vlastní zdroj
Implementujte NyonCode\WireCore\Core\Data\DataSource. Každá metoda dostane
plán, takže zdroj rozhoduje, kolik z něj splní:
use NyonCode\WireCore\Core\Data\DataSource; final class ReportingApiSource implements DataSource{ public function paginate(QueryPlan $plan, PagingRequest $paging): LengthAwarePaginator|Paginator|CursorPaginator; public function get(QueryPlan $plan): Collection; public function chunk(QueryPlan $plan, int $size, callable $callback): void; public function count(QueryPlan $plan): int; public function resolveRecord(int|string $key): ?RecordContract; public function resolveRecords(array $keys): Collection; public function capabilities(): CapabilitySet; public function changeToken(QueryPlan $plan): ?string; }
Dvě z nich stojí za vysvětlení, protože jejich tvar není samozřejmý:
chunk() existuje odděleně od get(), aby export sta tisíc řádků zůstal
ohraničený v paměti. get() materializuje; chunk() streamuje a zastaví se
dřív, když callback vrátí false.
changeToken() smí vrátit null a null je skutečná odpověď, ne selhání:
znamená, že zdroj nemá levný způsob, jak zjistit, že se data pohnula — polling
tedy porovná řádky sám místo zkratky přes token.
Záznamy
Zdroj vrací řádky jako RecordContract — getKey(), get('dot.path'),
toArray(), unwrap(). Ve stacku jsou dvě implementace: EloquentRecord obaluje
model, ArrayRecord pole.
Váš vlastní kód dál dostává modely. Framework rozbaluje na hranici, takže closure akce si drží svoji obvyklou signaturu:
Action::make('archive') ->action(fn (Model $record) => $record->archive());
Nad CollectionDataSource není co rozbalit, takže tabulka předá closure
CollectionRow sestavený z pole. Signatura platí; co closure s řádkem udělá, je
na ní — $record->archive() na něm neexistuje a $record->save() vyhodí výjimku
místo předstíraného zápisu.
Co zůstává jen pro Eloquent
Dvě cesty si záměrně nechávají builder místo kontraktu a obě jsou případy, které kontrakt vyjádřit neumí:
- Rollupy nad výběrem replikují agregační subquery na klíčovaný dotaz.
- Zápisy fill handlu berou pesimistický zámek řádku (
SELECT … FOR UPDATE).
Vlastní zdroj nedostane ani jedno.