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); } }