585 lines
17 KiB
PHP
585 lines
17 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"],
|
|
"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"],
|
|
"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(?callable $filterFunc): void {
|
|
if ($filterFunc === null) $this->filterCtx = null;
|
|
else $this->filterCtx = func::_prepare($filterFunc);
|
|
}
|
|
|
|
/** @var array */
|
|
protected $mapCtx;
|
|
|
|
function pp_setMapFunc(?callable $mapFunc): void {
|
|
if ($mapFunc === null) $this->mapCtx = null;
|
|
else $this->mapCtx = func::_prepare($mapFunc);
|
|
}
|
|
|
|
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|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 ensureArray($row): array {
|
|
if (!is_array($row)) {
|
|
if ($row instanceof BaseArray) $row = $row->array();
|
|
elseif ($row instanceof Iterator) $row = iterator_to_array($row);
|
|
else throw ValueException::unexpected_type("array", $row);
|
|
}
|
|
return $row;
|
|
}
|
|
|
|
protected function nextRow(): ?array {
|
|
$filterCtx = $this->filterCtx;
|
|
$mapCtx = $this->mapCtx;
|
|
$row = $this->_currentRow($this->rowKey);
|
|
if ($filterCtx !== null) {
|
|
if (!func::_call($filterCtx, [$row, $this->rowKey])) return null;
|
|
}
|
|
if ($mapCtx !== null) $row = func::_call($mapCtx, [$row]);
|
|
else $row = $this->ensureArray($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->origRow = $this->nextRow();
|
|
try {
|
|
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->printHeader($this->headers);
|
|
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;
|
|
}
|
|
|
|
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 avant sa "cuisine" */
|
|
protected $origRow;
|
|
|
|
/** @var array */
|
|
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 {
|
|
return v::tr($this->row($row));
|
|
}
|
|
|
|
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 {
|
|
$result = A::get($this->results, $this->col);
|
|
$valid = $result === null || $result["valid"];
|
|
return v::td([
|
|
"class" => ["danger" => !$valid],
|
|
$this->col($value),
|
|
]);
|
|
}
|
|
|
|
function col($value): ?iterable {
|
|
#XXX formatter selon le schéma, ou en utilisant des méthodes définies dans
|
|
# cette classe
|
|
return q($value);
|
|
}
|
|
}
|