5. Vlastní DataSource
Když data nejsou v SQL ani v poli, napíše se vlastní zdroj. Grid o něm nechce vědět nic víc,
než co je v rozhraní DataSource.
Ukázka
Kurzovní lístek čtený z CSV souboru vedle komponenty.
Zdrojový kód
<?php
declare(strict_types=1);
namespace App\Components\Docs\Data\CustomSourceGridControl;
use Xart\Grid\DataOutput\DataOutput;
use Xart\Grid\DataOutput\Row;
use Xart\Grid\DataSource\AbstractDataSource;
use Xart\Grid\DataSource\ArrayDataSource;
use Xart\Grid\Exception\RuntimeException;
use Xart\Grid\Query\Query;
/**
* Vlastní zdroj dat gridu – čte řádky z CSV souboru (kapitola 5 dokumentace).
*
* Ukazuje celé rozhraní `DataSource`: co která metoda musí umět a kdo ji volá. Filtrování, řazení
* a stránkování v paměti tady nepíšeme znovu – umí je `ArrayDataSource`, kterému je předáme.
* Kdyby se data do paměti nevešla (velký soubor, vzdálené API), musela by si každá z metod poradit
* sama a podmínky z `Query` přeložit do dotazu na zdroj.
*
* Dědíme z `AbstractDataSource`, takže máme hotové `findRowBy()` (odvozené z `findRowsBy()`),
* `getIdentifierField()` a převod pole na `Row`.
*/
final class CsvDataSource extends AbstractDataSource
{
private ArrayDataSource $rows;
/**
* @param string $file Cesta k CSV souboru; první řádek jsou názvy sloupců (aliasy).
* @param string $identifierField Alias sloupce, který řádky jednoznačně identifikuje.
*/
public function __construct(string $file, string $identifierField = 'id')
{
$this->identifierField = $identifierField;
$this->rows = new ArrayDataSource($this->read($file), $identifierField);
}
/** Řádky pro zobrazení. `$paginate === false` znamená „všechny“ – tak si o data říká export. */
public function getData(Query $query, bool $paginate = true): DataOutput
{
return $this->rows->getData($query, $paginate);
}
/** Kolik řádků zdroj má celkem – bez ohledu na filtry. Vypisuje se jako „Vyfiltrováno z N“. */
public function getTotalCount(): int
{
return $this->rows->getTotalCount();
}
/** Kolik řádků projde filtry. Podle toho se počítají stránky. */
public function getFilteredCount(Query $query): int
{
return $this->rows->getFilteredCount($query);
}
/** Které sloupce zdroj nabízí. Grid podle nich hlídá, že sloupec s daným aliasem opravdu existuje. */
public function getAliases(): array
{
return $this->rows->getAliases();
}
/**
* Dohledání řádků podle hodnot v jednom sloupci – tudy si grid bere zatržené řádky pro hromadné
* akce a řádek pro modál.
*
* @param list<mixed> $values
* @return list<Row>
*/
public function findRowsBy(string $alias, array $values): array
{
return $this->rows->findRowsBy($alias, $values);
}
/**
* Přeuspořádání drag & drop. Zdroj, který zapisovat neumí, může metodu odmítnout – stačí
* v gridu nepoužít sloupec pro řazení a nikdo ji nezavolá.
*/
public function moveOrdering(string $orderingColumn, int $movedOrdering, int $targetOrdering): void
{
throw new RuntimeException('Pořadí řádků v CSV souboru měnit neumíme.');
}
/**
* Načte CSV do pole „alias => hodnota“. Čísla převádíme na čísla (i s desetinnou čárkou):
* grid pracuje s tím, co zdroj vrátí, takže číslo uložené jako text by se i řadilo jako text.
*
* @return list<array<string, mixed>>
*/
private function read(string $file): array
{
$handle = fopen($file, 'r');
if ($handle === false) {
throw new RuntimeException(sprintf('Soubor %s se nepodařilo otevřít.', $file));
}
$header = fgetcsv($handle, separator: ';', escape: '');
if (!is_array($header)) {
fclose($handle);
return [];
}
$rows = [];
$id = 0;
while (($line = fgetcsv($handle, separator: ';', escape: '')) !== false) {
$row = ['id' => ++$id];
foreach ($header as $index => $alias) {
$row[(string) $alias] = self::value($line[$index] ?? null);
}
$rows[] = $row;
}
fclose($handle);
return $rows;
}
/** Hodnota z buňky CSV: co vypadá jako číslo (i s čárkou), vrátíme jako číslo. */
private static function value(?string $value): string|float|int|null
{
if ($value === null || trim($value) === '') {
return null;
}
$value = trim($value);
$number = str_replace(',', '.', $value);
if (!is_numeric($number)) {
return $value;
}
return str_contains($number, '.') ? (float) $number : (int) $number;
}
}
<?php
declare(strict_types=1);
namespace App\Components\Docs\Data\CustomSourceGridControl;
use Xart\Grid\Button\ButtonManager;
use Xart\Grid\Column\ColumnManager;
use Xart\Grid\Control\GridControl;
use Xart\Grid\DataOutput\Row;
use Xart\Grid\DataSource\DataSource;
/**
* Dokumentace, kapitola 5 – Vlastní DataSource.
*
* Grid nad daty, která nejsou ani v databázi, ani v poli v kódu: čtou se z CSV souboru vlastním
* zdrojem {@see CsvDataSource}. Pro grid samotný se nic nemění – dostane `DataSource` a je mu
* jedno, odkud si řádky bere.
*/
class CustomSourceGridControl extends GridControl
{
public function createDataSource(): DataSource
{
return new CsvDataSource(__DIR__ . '/rates.csv');
}
public function columns(ColumnManager $cm): void
{
$cm->add('code', 'Kód');
$cm->add('currency', 'Měna');
$cm->add('country', 'Země');
$cm->addNumber('amount', 'Množství', 0);
$cm->addNumber('rate', 'Kurz', 2, 'Kč');
}
public function buttons(ButtonManager $bm): void
{
}
public function rows(Row $row): void
{
}
public function render(): void
{
$this->template->render(__DIR__ . '/CustomSourceGridControl.latte');
}
}
code;currency;country;amount;rate
USD;americký dolar;USA;1;22,84
EUR;euro;eurozóna;1;24,31
GBP;libra šterlinků;Velká Británie;1;29,05
CHF;švýcarský frank;Švýcarsko;1;25,92
PLN;zlotý;Polsko;1;5,71
HUF;forint;Maďarsko;100;6,04
JPY;jen;Japonsko;100;14,88
CAD;kanadský dolar;Kanada;1;16,42
AUD;australský dolar;Austrálie;1;14,79
NOK;koruna;Norsko;1;2,11
SEK;koruna;Švédsko;1;2,18
DKK;koruna;Dánsko;1;3,26
CNY;žen-min-pi;Čína;1;3,17
TRY;lira;Turecko;100;6,73
BRL;real;Brazílie;1;4,05
Co musí zdroj umět
getData($query, $paginate) | Řádky k zobrazení; $paginate === false = všechny (tak si říká export). |
getTotalCount() | Kolik je záznamů celkem, bez filtrů. |
getFilteredCount($query) | Kolik jich projde filtry – podle toho se počítají stránky. |
getAliases() | Které sloupce zdroj nabízí; grid podle nich hlídá columns(). |
getIdentifierField() | Který sloupec řádky identifikuje. |
findRowsBy(), findRowBy() | Dohledání řádků – zatržené řádky akcí, řádek modálu. |
moveOrdering() | Drag & drop; bez sloupce pro řazení ji nikdo nezavolá. |
Pro akce mazání a úprav (kapitola 38) navíc WritableDataSource
(deleteRow(), updateRow()).
Co nemusíte psát
AbstractDataSource dá findRowBy(),
getIdentifierField() a převod pole na Row (včetně uzamčení buňky
s identifikátorem). Filtrování a řazení v paměti umí ArrayDataSource – ukázkový
CsvDataSource ho uvnitř používá a řeší jen načtení souboru.
Když se data do paměti nevejdou
Pak musí každá metoda přeložit Query do dotazu na zdroj:
conditionGroup je strom podmínek spojených AND/OR,
order je alias => ASC|DESC, offset
a limit je stránkování.
Co zdroj neumí, ať radši ohlásí výjimkou – tiché ignorování podmínky znamená víc řádků,
než uživatel čeká. A vracejte hodnoty ve správném typu: kurz s desetinnou čárkou se proto
při načtení převádí na float, jinak by se řadil abecedně.