Produkty Objednávky Zákazníci Dokumentace
Nepřihlášen Uživatel 1 Uživatel 2 Uživatel 3
Část II · Zdroje dat

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 app/Modules/AdminModule/Components/Docs/Data/CustomSourceGridControl/CsvDataSource.php
<?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 app/Modules/AdminModule/Components/Docs/Data/CustomSourceGridControl/CustomSourceGridControl.php
<?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');
    }
}
csv app/Modules/AdminModule/Components/Docs/Data/CustomSourceGridControl/rates.csv
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ě.