2
\$\begingroup\$

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

}
\$\endgroup\$
0

1 Answer 1

3
\$\begingroup\$

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.

  1. Why is it valuable to return true in setOrientation(). You don't use the return value, but if you did, the conditional true would give a false impression of success. It is a code smell to me.

  2. 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.

  3. strlen() === 0 can be reduced to !strlen() without harming code readability.

  4. if (count($array) > 0) and if (count($array) !== 0) can always be replaced with if ($array).

  5. Assuming $headers is always an indexed array, you don't need to declare $cell_counter -- just declare it as the key in your foreach().

  6. 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)]);
    
  7. I recommend a more indicative variable name than $column and $row to contain integers. I associate these terms with arrays of data which are accessed from within a 2d array. Perhaps $rowNum, $rowNo, or $rowNumber for a few examples.

  8. That last while() loop should be a for(), foreach(), or array_reduce(). It doesn't make sense to call count() on every iteration when the array's size doesn't change.

\$\endgroup\$
4
  • \$\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 bool should return void but 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\$ Commented 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\$ Commented 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\$ Commented 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\$ Commented May 26, 2023 at 16:17

You must log in to answer this question.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.