After my previous question I made several changes and came up with this. Remember: I wrote this for PHP 7.0.33. I'm aware I should upgrade, save the advice :)
class Table
{
private $orientation = "hz"; // "vt" not implemented yet
private $table_title = "";
private $headers = array();
private $data = array();
private $footers = array();
private $tabs = 0;
/**
* Holds default HTML classes
* @var array
*
* Should be private but PHP 7 blah blah... save the advice, I'm aware
*/
const HTML_TABLE_CLASSES = [
"table" => "table",
"title" => "table-title",
"header_row" => "table-header",
"header_cell" => "table-header-cell",
"body" => "table-body",
"row" => "table-row",
"cell" => "table-row-cell",
"footer_row" => "table-footer",
"footer_cell" => "table-footer-cell"
];
/**
* Holds user defined attributes for HTML elements
* @var array
*/
private $html_attributes = [
"table" => [],
"title" => [],
"header_row" => [],
"header_cell" => [],
"body" => [],
"row" => [],
"cell" => [],
"footer_row" => [],
"footer_cell" => []
];
public function __construct(
string $orientation = "hz",
int $tabs = 0,
array $tableAttrs = []
) {
$this->setOrientation($orientation);
$this->setTabs($tabs);
$this->setTableAttributes($tableAttrs);
}
public function setOrientation(string $orientation = "hz"): bool
{
if ($orientation === "vt") {
$this->orientation = "vt";
}
return true;
}
public function setTabs(int $tabs = 0): bool
{
$this->tabs = ($tabs > 0) ? $tabs : 0;
return true;
}
public function setTableAttributes(array $attrs = []): bool
{
$this->html_attributes["table"] = $attrs;
return true;
}
public function setTitle(string $table_title, array $attributes = []): bool
{
$this->table_title = $table_title;
$this->html_attributes['title'] = $attributes;
return true;
}
private function getTitle(): string
{
if (strlen($this->table_title) === 0) {
return "";
}
return $this->getHtmlDiv(
$this->table_title,
$this->getHtmlAttributes("title"),
1
);
}
public function setHeaders(
array $headers,
array $row_attributes = [],
array $cell_attributes = []
): bool {
if (
(count($headers) > 0) &&
(count($headers) !== count($headers, COUNT_RECURSIVE))
) {
// throw new Exception("Headers array should be a simple array");
trigger_error("Headers array should be a simple array. Headers will be dismissed.", E_USER_WARNING);
return false;
}
$this->headers = $headers;
$this->html_attributes['header_row'] = $row_attributes;
$this->html_attributes['header_cell'] = $cell_attributes;
return true;
}
private function getHeaders(): string
{
$output = "";
$cell_counter = 0;
if (count($this->headers) !== 0) {
foreach($this->headers as $cell_data) {
$output .= $this->getHtmlDiv(
$cell_data,
$this->getHtmlAttributes("header_cell", $cell_counter),
2
);
$cell_counter++;
}
$output = $this->getHtmlDiv(
$output,
$this->getHtmlAttributes("header_row"),
1,
true
);
}
return $output;
}
public function setData(
array $data,
array $row_attributes = [],
array $cell_attributes = []
): bool {
$this->checkData($data);
$this->data = $data;
$this->html_attributes['row'] = $row_attributes;
$this->html_attributes['cell'] = $cell_attributes;
return true;
}
private function checkData(array $data = []): bool
{
/*
if (count($data) < 1) {
throw new Exception("With no data there is no table to render");
}
*/
if (
(count($data) > 1) &&
count($data) === count($data, COUNT_RECURSIVE)
) {
// throw new Exception("Table data array has the wrong format: it must be a multidimensional array");
trigger_error(
"Table data array has the wrong format: it must be a multidimensional array. Set to empty array.",
E_USER_WARNING
);
$this->data = [];
}
return true;
}
private function getData(): string
{
$this->checkData($this->data);
$body_build = "";
$row_build = "";
$output = "";
$col_count = 0;
if (count($this->headers) > 0) {
$col_count = count($this->headers);
} else {
$col_count = count($this->data[array_keys($this->data)[0]]);
}
$row_counter = 0;
foreach ($this->data as $data) { // row
$row_build = "";
$cell_counter = 0;
foreach ($data as $cell_data) { // cell
if ($cell_counter < $col_count) {
$row_build .= $this->getHtmlDiv(
$cell_data ?? "",
$this->getHtmlAttributes("cell", $row_counter, $cell_counter),
3
);
$cell_counter++;
}
}
$row_build .= $this->getEmptyCells("footer_cell", $cell_counter, 2);
$body_build .= $this->getHtmlDiv(
$row_build,
$this->getHtmlAttributes("row", $row_counter, 0),
2,
true
);
$row_counter++;
}
$output .= $this->getHtmlDiv(
$body_build,
$this->getHtmlAttributes("body"),
1,
true
);
return $output;
}
public function setBodyAttributes(array $attrs = []): bool
{
$this->html_attributes["body"] = $attrs;
return true;
}
public function setFooters(
array $footers = [],
array $row_attributes = [],
array $cell_attributes = []
): bool {
if (count($footers) > 0) {
if (
(count($footers) > 0) &&
(count($footers) !== count($footers, COUNT_RECURSIVE))
) {
// throw new Exception("Footers should be a simple array");
trigger_error("Footers should be a simple array. Footers will be dismissed", E_USER_WARNING);
return false;
}
$this->footers = $footers;
$this->html_attributes['footer_row'] = $row_attributes;
$this->html_attributes['footer_cell'] = $cell_attributes;
}
return true;
}
private function getFooters(int $col_count): string
{
$output = "";
$cell_counter = 0;
if (count($this->footers) > 0) {
foreach($this->footers as $cell_data) {
if ($cell_counter < $col_count) {
$output .= $this->getHtmlDiv(
$cell_data,
$this->getHtmlAttributes("footer_cell", $cell_counter),
2
);
$cell_counter++;
}
}
$output .= $this->getEmptyCells("footer_cell", $cell_counter, 2);
$output = $this->getHtmlDiv(
$output,
$this->getHtmlAttributes("footer_row"),
1,
true
);
}
return $output;
}
public function get(): string
{
$this->checkData($this->data);
$output = "";
$output .= $this->getTitle();
if ($this->orientation === "hz") {
/**
* column count: Anything beyond $col_count will be dismissed.
* A warning will be added.
*/
if (count($this->headers) > 0) {
$col_count = count($this->headers);
} else {
$col_count = count($this->data[array_keys($this->data)[0]]);
}
$output .= $this->getHeaders();
$output .= $this->getData($col_count);
$output .= $this->getFooters($col_count);
}
// get table and return
return $this->getHtmlDiv(
$output,
$this->getHtmlAttributes("table"),
0,
true
);
}
private function getHtmlAttributes(string $section, int $row = 0, int $column = 0): array
{
$output = [];
if (strpos($section, "cell") !== false) {
if (
array_key_exists($row, $this->html_attributes[$section]) &&
array_key_exists($column, $this->html_attributes[$section][$row])
) {
$output = $this->html_attributes[$section][$row][$column];
}
} else { // table, title, header, body, row, footer
$output = $this->html_attributes[$section];
}
if ( // set default. trigger user warning?
!array_key_exists("class", $output) ||
(strlen($output["class"]) === 0)
) {
$output["class"] = self::HTML_TABLE_CLASSES[$section];
}
return $output;
}
private function getHtmlDiv(
string $content = "",
array $attrs = [],
int $tabcount = 0,
bool $add_newline = false
): string {
$output = "\n" . str_repeat(" ", $this->tabs + $tabcount) . "<div";
foreach ($attrs as $attr => $value) {
$output .= " $attr='$value'";
}
$output .= ">";
$output .= $content;
if ($add_newline) {
$output .= "\n" . str_repeat(" ", $this->tabs + $tabcount);
}
$output .= "</div>";
return $output;
}
private function getEmptyCells(
string $section,
int $cell_counter,
int $tabs
): string {
$output = "";
while ($cell_counter < count($this->headers)) {
$output .= $this->getHtmlDiv(
"",
$this->getHtmlAttributes($section, $cell_counter),
$tabs
);
$cell_counter++;
}
return $output;
}
}
1 Answer 1
I haven't reviewed the script from a high altitude for its design architecture, but from my phone I found a few points to mention.
Why is it valuable to return
trueinsetOrientation(). You don't use the return value, but if you did, the conditionaltruewould give a false impression of success. It is a code smell to me.In
setTabs(), you have$this->tabs = ($tabs > 0) ? $tabs : 0;. The parentheses are unneeded functionally. Maybe you'd consider throwing an exception when a negative integer is passed in or instead of falling back to 0, you could fallback to whatever the cached value is. Or if you want to enforce a non-negative value,max()can be used.$this->tabs = max($tabs, 0);Again, I don't see the need for a return value.strlen() === 0can be reduced to!strlen()without harming code readability.if (count($array) > 0)andif (count($array) !== 0)can always be replaced withif ($array).Assuming
$headersis always an indexed array, you don't need to declare$cell_counter-- just declare it as the key in yourforeach().I might consolidate:
if (count($this->headers) > 0) { $col_count = count($this->headers); } else { $col_count = count($this->data[array_keys($this->data)[0]]); }to
$col_count = count($this->headers ?: $this->data[array_key_first($this->data)]);I recommend a more indicative variable name than
$columnand$rowto contain integers. I associate these terms with arrays of data which are accessed from within a 2d array. Perhaps$rowNum,$rowNo, or$rowNumberfor a few examples.That last
while()loop should be afor(),foreach(), orarray_reduce(). It doesn't make sense to callcount()on every iteration when the array's size doesn't change.
-
\$\begingroup\$ Thanks for your time. Method declaration shouldn't always include return type? Trying to accomplish PSR-12 as much as I can methods returning
boolshould returnvoidbut that's not allowed in PHP 7.0.33 (added in PHP 7.1). Point 8 made me realise that if headers are not set that comparison may sometimes not happen. The correct comparison value should be$col_count. ''$this->headers` can be an associative array... it's ok, keys are ignored. \$\endgroup\$julio– julio2023年05月26日 10:18:56 +00:00Commented May 26, 2023 at 10:18 -
\$\begingroup\$ Then I suppose you simply do not declare a return type until you upgrade your PHP version. \$\endgroup\$mickmackusa– mickmackusa2023年05月26日 14:05:05 +00:00Commented May 26, 2023 at 14:05
-
\$\begingroup\$ I've been reading about this topic but couldn't find what I was looking. So it's ok to not declare method return type? If return type is mandatory I guess I should update method's code to return corresponding values... \$\endgroup\$julio– julio2023年05月26日 14:25:41 +00:00Commented May 26, 2023 at 14:25
-
\$\begingroup\$ On point 2, I decided to
trigger_error()when a negative value is passed and set it to 0. \$\endgroup\$julio– julio2023年05月26日 16:17:15 +00:00Commented May 26, 2023 at 16:17