nur-sery/nur_src/v/bs3/vc/CTable.php

658 lines
19 KiB
PHP

<?php
namespace nur\v\bs3\vc;
use Exception;
use Iterator;
use nur\A;
use nur\b\coll\BaseArray;
use nur\b\params\IParametrable;
use nur\b\params\Tparametrable1;
use nur\b\ValueException;
use nur\data\types\md_utils;
use nur\data\types\Metadata;
use nur\func;
use nur\iter;
use nur\SL;
use nur\v\base\ComponentPrintable;
use nur\v\v;
use nur\v\vo;
/**
* Class CTable: affiche un tableau de données
*/
class CTable extends ComponentPrintable implements IParametrable {
use Tparametrable1;
const COLS = null;
const EXCLUDE_COLS = null;
const ADD_COLS = null;
const HEADERS = null;
const ADD_HEADERS = null;
/** @var array schéma des données à afficher */
const SCHEMA = null;
const TABLE_CLASS = "table-bordered";
const AUTOPRINT = null;
/** @var ?array schéma des informations supplémentaires */
const DATA_SCHEMA = null;
/**
* retourner la valeur de COLS construite dynamiquement. cette méthode n'est
* appelée que si c'est nécessaire
*/
protected function COLS(): ?array {
return null;
}
/**
* retourner la valeur de HEADERS construite dynamiquement. cette méthode
* n'est appelée que si c'est nécessaire
*/
protected function HEADERS(): ?array {
return null;
}
function __construct(?iterable $rows=null, ?array $params=null) {
self::set_parametrable_params_defaults($params, [
"cols" => static::COLS,
"exclude_cols" => static::EXCLUDE_COLS,
"add_cols" => static::ADD_COLS,
"headers" => static::HEADERS,
"add_headers" => static::ADD_HEADERS,
"schema" => static::SCHEMA,
"table_class" => static::TABLE_CLASS,
"autoprint" => static::AUTOPRINT,
"data_schema" => static::DATA_SCHEMA,
]);
if ($rows === null) $rows = $this->defaultRows();
A::set_nn($params, "rows", $rows);
[$params, $data] = $this->splitParametrableParams($params);
if ($data) A::merge($params["data"], $data);
$this->initParametrableParams($params);
if ($this->ppAutoprint) $this->print();
}
protected function defaultRows(): ?iterable {
return null;
}
const PARAMETRABLE_PARAMS_SCHEMA = [
"rows" => ["?iterable", null, "source des lignes à afficher"],
"filter_func" => ["?callable", null, "fonction permettant de filter les éléments à afficher"],
"map_func" => ["?callable", null, "fonction permettant de mapper les éléments"],
"map" => ["?array", null, "tableau permettant de sélectionner les éléments"],
"cols" => ["?array", null, "colonnes à extraire et à afficher"],
"exclude_cols" => ["?array", null, "colonnes à exclure de la liste calculée automatiquement"],
"add_cols" => ["?array", null, "colonnes à ajouter à la liste calculée automatiquement"],
"headers" => ["?array", null, "en-têtes correspondant aux colonnes"],
"add_headers" => ["?array", null, "en-têtes à ajouter à la liste calculée automatiquement"],
"schema" => ["?array", null, "schéma des données à afficher"],
"table_class" => ["?array", null, "classe CSS du tableau"],
"table_attrs" => ["?array", null, "autres attributs à placer sur le tableau"],
"before_table" => ["?content", null, "Contenu à afficher avant le tableau s'il y a des lignes à afficher"],
"row_func" => ["?callable", null, "fonction avec la signature (\$vs, \$row, \$rawRow) retournant la ligne à afficher"],
"col_func" => ["?callable", null, "fonction avec la signature (\$vs, \$value, \$col, \$index, \$row, \$rawRow) retournant la colonne à afficher"],
"after_table" => ["?content", null, "Contenu à afficher après le tableau s'il y a des lignes à afficher"],
"no_data" => ["?content", null, "Contenu à afficher s'il n'y a pas de lignes à afficher"],
"on_exception" => ["?callable", null, "fonction à appeler en cas d'exception"],
"autoprint" => ["bool", false, "faut-il afficher automatiquement le tableau"],
"data" => ["?array", null, "donnés supplémentaires utilisées pour l'affichage"],
"data_schema" => ["?array", null, "schéma des données supplémentaire"],
];
const COL_SCHEMA = [
"name" => ["?string", null, "Nom de la colonne"],
"pkey" => ["?string", null, "Clé permettant de calculer la valeur effective de la colonne"],
];
private static $col_md;
const HEADER_SCHEMA = [
"name" => ["string", null, "Nom de l'en-tête"],
"span" => ["?int", null, "nombre de colonnes que cet en-tête doit occuper"],
"label" => ["?string", null, "Libellé de l'en-tête",
"desc" => "vaut [name] par défaut",
],
];
private static $header_md;
/** @var Metadata schéma des données */
protected $md;
function pp_setSchema($schema): void {
$this->md = Metadata::with($schema);
}
/** @var ?iterable */
protected $ppRows;
/** @var array */
protected $filterCtx;
function pp_setFilterFunc($filterFunc): void {
if ($filterFunc === null) $this->filterCtx = null;
else $this->filterCtx = func::_prepare($filterFunc);
}
/** @var array */
protected $mapCtx;
function pp_setMapFunc($mapFunc): void {
if ($mapFunc === null) $this->mapCtx = null;
else $this->mapCtx = func::_prepare($mapFunc);
}
/** @var ?array */
protected $ppMap;
protected $ppCols;
protected $ppExcludeCols;
protected $ppAddCols;
protected $ppHeaders;
protected $ppAddHeaders;
/** @var array classe CSS du tableau */
protected $ppTableClass;
protected function getTableClass(): array {
return $this->ppTableClass;
}
/** @var array */
protected $ppTableAttrs;
/** @var array|string */
protected $ppBeforeTable;
/** @var array */
protected $rowCtx;
function pp_setRowFunc($rowFunc): void {
if ($rowFunc === null) $this->rowCtx = null;
else $this->rowCtx = func::_prepare($rowFunc);
}
/** @var array */
protected $colCtx;
function pp_setColFunc($colFunc): void {
if ($colFunc === null) $this->colCtx = null;
else $this->colCtx = func::_prepare($colFunc);
}
/** @var array|string */
protected $ppAfterTable;
/** @var array|string */
protected $ppNoData;
/** @var callable */
protected $ppOnException;
/** @var bool */
protected $ppAutoprint;
/** @var Metadata */
protected $dataSchema;
function pp_setDataSchema($dataSchema): void {
$this->dataSchema = Metadata::with($dataSchema);
}
/** @var mixed données supplémentaires utilisées pour l'affichage */
protected $data;
function pp_setData(?array $data): void {
$this->data = $data;
}
protected function afterSetParametrableParams(array $modifiedKeys, ?Metadata $md=null): void {
if (self::was_parametrable_param_modified($modifiedKeys
, "cols", "exclude_cols", "add_cols"
, "headers", "add_headers")) {
$this->cols = null;
$this->headers = null;
}
if (self::was_parametrable_param_modified($modifiedKeys, "data", "data_schema")) {
if ($this->dataSchema !== null) {
$this->dataSchema->ensureSchema($this->data);
}
}
}
/** @var array liste des colonnes à extraire des lignes */
protected $cols;
/** @var array informations sur les colonnes à extraire des lignes */
protected $colInfos;
/** @var array en-têtes correspondant aux colonnes extraites */
protected $headers;
protected function updateColInfos(?array &$cols, ?array &$colInfos): void {
# extraire les données de colonnes pour que $cols soit juste une liste de
# colonnes
if ($cols === null) return;
if ($colInfos === null) $colInfos = [];
$colKeys = [];
$index = 0;
foreach ($cols as $key => $col) {
if ($key === $index) {
$index++;
if ($col === null) {
# colonne "nulle", à prendre telle quelle
$colKeys[] = null;
} elseif (!array_key_exists($col, $colInfos)) {
$colKeys[] = $col;
$colInfos[$col] = null;
}
} else {
if (is_array($col) && array_key_exists("name", $col)) {
$key = $col["name"];
}
if (!array_key_exists($key, $colInfos)) $colKeys[] = $key;
$colInfos[$key] = $col;
}
}
$cols = $colKeys;
}
protected function resolveColsHeaders($row): bool {
if ($this->cols !== null || $this->headers !== null) return false;
$schema = $this->md;
$cols = $this->ppCols;
$colsExcludes = $this->ppExcludeCols;
if ($cols === null) $cols = $this->COLS();
if ($cols === null && $schema !== null) {
$cols = A::xselect($schema->getKeys(), null, $colsExcludes);
}
$this->updateColInfos($cols, $colInfos);
$headers = $this->ppHeaders;
$sfields = null;
if ($headers === null) $headers = $this->HEADERS();
if ($headers === null) {
if ($schema !== null) {
# ici, $cols est forcément défini, à cause de la commande ci-dessus
$headers = [];
$sfields = $schema->getSfields();
foreach ($cols as $col) {
$sfield = A::get($sfields, $col);
$headers[] = $sfield !== null? $sfield["header"]: $col;
}
} elseif ($cols !== null) {
$headers = $cols;
}
}
$skipRow = false;
if ($cols === null || $headers === null) {
# si pas de colonnes ou de headers, prendre la première ligne si
# c'est une séquence ou ses clés si c'est un tableau associatif
if (A::is_seq($row)) {
$skipRow = true;
$keys = A::xselect($row, null, $colsExcludes);
if ($headers === null) $headers = $keys;
if ($cols === null) $cols = array_keys($keys);
} else {
$row = A::xselect_keys($row, null, $colsExcludes);
if ($headers === null) $headers = array_keys($row);
if ($cols === null) $cols = array_keys($row);
}
}
# ces cas ne devraient pas se produire puisqu'ils devraient être récupérés
# par la section no_contents, mais bon... au cas où
if ($headers === null) $headers = [];
if ($cols === null) $cols = [];
$addCols = $this->ppAddCols;
$this->updateColInfos($addCols, $colInfos);
$addHeaders = $this->ppAddHeaders;
if ($addCols !== null && $addHeaders !== null) {
# si on fournit les deux, les intégrer sans modification
A::merge($cols, $addCols);
A::merge($headers, $addHeaders);
} elseif ($addCols !== null) {
# si on ne donne que des colonnes supplémentaires, inférer les en-têtes
# à rajouter
foreach ($addCols as $col) {
$cols[] = $col;
if ($col === null) {
$headers[] = "";
} elseif ($sfields !== null) {
$sfield = A::get($sfields, $col);
$headers[] = $sfield !== null? $sfield["header"]: $col;
} else {
$headers[] = $col;
}
}
} elseif ($addHeaders !== null) {
# si on ne donne que des en-têtes supplémentaires, rajouter des colonnes
# nulles pour compenser
foreach ($addHeaders as $header) {
$cols[] = null;
$headers[] = $header;
}
}
md_utils::ensure_md(self::$col_md, self::COL_SCHEMA)
->eachEnsureSchema($colInfos);
md_utils::ensure_md(self::$header_md, self::HEADER_SCHEMA)
->eachEnsureSchema($headers);
$this->cols = $cols;
$this->colInfos = $colInfos;
$this->headers = $headers;
return $skipRow;
}
protected function _rewindRows(): bool {
return iter::rewind($this->ppRows);
}
protected function _validRow(): bool {
return iter::valid($this->ppRows);
}
protected function _currentRow(&$key=null) {
return iter::current($this->ppRows, $key);
}
protected function _nextRow(): void {
iter::next($this->ppRows);
}
protected function nextRow(): ?array {
$filterCtx = $this->filterCtx;
$mapCtx = $this->mapCtx;
$map = $this->ppMap;
$row = $this->rawRow;
if ($filterCtx !== null) {
if (!func::_call($filterCtx, [$row, $this->rowKey, $this->rowIndex])) return null;
}
if ($mapCtx !== null) {
# si la valeur est séquentielle, c'est une clé dans $row
# si la valeur est associative et que c'est une fonction, elle est appelée
# avec la valeur
$map = func::_call($mapCtx, [$row, $this->rowKey, $this->rowIndex]);
}
if ($map !== null) {
$row = A::with($row);
$index = 0;
$mapped = [];
foreach ($map as $key => $value) {
if ($key === $index) {
$index++;
if ($value === null) $mapped[] = null;
else $mapped[$value] = A::get($row, $value);
} elseif (is_callable($value)) {
$mapped[$key] = func::call($value, A::get($row, $key), $key, $row, $this->rowKey, $this->rowIndex);
} else {
$mapped[$key] = $value;
}
}
$row = $mapped;
} else {
$row = A::with($row);
}
return $row;
}
protected function getRowValues(array $row): array {
$havePkey = false;
if ($this->colInfos !== null) {
foreach ($this->colInfos as $colInfo) {
if ($colInfo["pkey"] !== null) {
$havePkey = true;
break;
}
}
}
if (!$havePkey) return $row;
$cooked = [];
foreach ($this->cols as $col) {
if ($col === null) {
$cooked[] = null;
} else {
$colInfo = A::get($this->colInfos, $col);
$pkey = $colInfo !== null? $colInfo["pkey"]: null;
if ($pkey !== null) $value = A::pget($row, $pkey);
else $value = $row[$col];
$cooked[$col] = $value;
}
}
# compléter avec les autres champs tels quels
foreach ($row as $key => $value) {
if (!array_key_exists($key, $cooked)) {
$cooked[$key] = $value;
}
}
return $cooked;
}
protected $results;
protected function ensureRowSchema(array &$row): void {
$md = $this->md;
if ($md !== null) {
$md->ensureSchema($row);
$md->verifix($row, $this->results, false, true);
#XXX il faut garder les champs "autres"!
#$row = $md->format($row);
}
}
protected function cookRow(array $row): array {
$cooked = $this->getRowValues($row);
$this->ensureRowSchema($cooked);
return $cooked;
}
function print(): void {
try {
$this->_rewindRows();
$this->rowIndex = 0;
$haveRows = false;
while ($this->_validRow()) {
$this->rawRow = $this->_currentRow($this->rowKey);
try {
$this->origRow = $this->nextRow();
if ($this->origRow === null) continue;
$skipRow = false;
if (!$haveRows) $skipRow = $this->resolveColsHeaders($this->origRow);
if (!$skipRow) {
$this->row = $this->cookRow($this->origRow);
if (!$haveRows) {
$haveRows = true;
vo::print($this->beforeTable());
vo::stable(["class" => "table", $this->table()]);
vo::sthead($this->thead());
$this->printBeforeHeader();
$this->printHeader($this->headers);
$this->printAfterHeader();
vo::ethead();
vo::stbody($this->tbody());
}
$this->printBeforeRow();
$this->printRow($this->row);
$this->printAfterRow();
$this->rowIndex++;
}
} finally {
$this->_nextRow();
}
}
if ($haveRows) {
vo::etbody();
vo::etable();
vo::print($this->afterTable());
} else {
vo::print($this->noData());
}
} catch (Exception $e) {
$onException = $this->ppOnException;
if ($onException !== null) func::call($onException, $e);
else throw $e;
}
}
/** contenu à afficher avant le tableau s'il y a des lignes à afficher */
function beforeTable(): ?iterable {
return $this->ppBeforeTable;
}
/** contenu à afficher après le tableau s'il y a des lignes à afficher */
function afterTable(): ?iterable {
return $this->ppAfterTable;
}
/** contenu à afficher s'il n'y a pas de lignes à afficher */
function noData(): ?iterable {
return $this->ppNoData;
}
function table(): ?iterable {
return SL::merge(["class" => $this->getTableClass()], $this->ppTableAttrs);
}
function thead(): ?iterable {
return null;
}
/** contenu à afficher avant la ligne */
function beforeHeader(): ?iterable {
return null;
}
function printBeforeHeader(): void {
$beforeHeaderContents = $this->beforeHeader();
if ($beforeHeaderContents !== null) vo::tr($beforeHeaderContents);
}
/** contenu à afficher après la ligne */
function afterHeader(): ?iterable {
return null;
}
function printAfterHeader(): void {
$afterHeaderContents = $this->afterHeader();
if ($afterHeaderContents !== null) vo::tr($afterHeaderContents);
}
function printHeader(array $headers): void {
vo::print($this->headerTr($headers));
}
function headerTr(array $headers): array {
return v::tr($this->headerRow($headers));
}
function headerRow(array $headers): ?iterable {
$vs = [];
foreach ($headers as $header) {
$label = $header["label"];
if ($label === null) $label = $header["name"];
$colspan = $header["span"];
$vs[] = v::th(["colspan" => $colspan, $label]);
}
return $vs;
}
function tbody(): ?iterable {
return null;
}
/** @var int index de la ligne courante */
protected $rowIndex;
/** @var string|int clé de la ligne courante */
protected $rowKey;
/** @var array la ligne originale, avant le mapping */
protected $rawRow;
/** @var array la ligne mappée avant sa "cuisine" */
protected $origRow;
/** @var array la ligne cuisinée */
protected $row;
/** contenu à afficher avant la ligne */
function beforeRow(): ?iterable {
return null;
}
function printBeforeRow(): void {
$beforeRowContents = $this->beforeRow();
if ($beforeRowContents !== null) vo::tr($beforeRowContents);
}
/** contenu à afficher après la ligne */
function afterRow(): ?iterable {
return null;
}
function printAfterRow(): void {
$afterRowContents = $this->afterRow();
if ($afterRowContents !== null) vo::tr($afterRowContents);
}
function printRow(array $row): void {
vo::print($this->rowTr($row));
}
function rowTr(array $row): array {
$vs = $this->row($row);
if ($this->rowCtx !== null) {
$vs = func::_call($this->rowCtx, [$vs, $row, $this->rawRow]);
}
return v::tr($vs);
}
function row(array $row): ?iterable {
$vs = [];
$this->index = 0;
foreach ($this->cols as $this->col) {
if ($this->col === null) $value = null;
else $value = $row[$this->col];
$vs[] = $this->colTd($value);
$this->index++;
}
return $vs;
}
/** @var int index de la colonne courante */
protected $index;
/** @var string|int clé de la colonne courante */
protected $col;
function colTd($value): array {
$vs = $this->col($value);
if ($this->colCtx !== null) {
$vs = func::_call($this->colCtx, [$vs, $value, $this->col, $this->index, $this->row, $this->rawRow]);
} else {
$result = A::get($this->results, $this->col);
$valid = $result === null || $result["valid"];
$vs= [
"class" => ["danger" => !$valid],
$vs,
];
}
return v::td($vs);
}
function col($value): ?iterable {
#XXX formatter selon le schéma, ou en utilisant des méthodes définies dans
# cette classe
return q($value);
}
}