0
\$\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\$
5
  • 2
    \$\begingroup\$ Why would a CodeReview question's title ever need [reviewed] in it? That doesn't uniquely describe what your script does. A title should endeavor to uniquely describe the script and nothing more. \$\endgroup\$ May 26 at 0:09
  • \$\begingroup\$ I agree with @mickmackusa. But when I tried to edit the title I was rewarded with this diagnostic: A question with that title already exists; please be more specific. \$\endgroup\$
    – J_H
    May 26 at 3:11
  • \$\begingroup\$ yeah i thought it was [closed] at a quick glance... \$\endgroup\$
    – slepic
    May 26 at 4:11
  • \$\begingroup\$ Well team, the correct thing to do IMO is to identify how this question differs from its previous version. Notice how it previously was not obeying the SRP from SOLID. Now it seems to be much better broken down into smaller methods. I haven't reviewed the new code, but the title should uniquely and intuitively identify this page. \$\endgroup\$ May 26 at 5:54
  • \$\begingroup\$ Well, I updated the title slightly to avoid confusions and misinterpretations. I apologize. I will implement versioning in the title. \$\endgroup\$
    – julio
    May 27 at 22:49

1 Answer 1

1
\$\begingroup\$

I haven't reviewed the script from a high altitude for is 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\$
    – julio
    May 26 at 10:18
  • \$\begingroup\$ Then I suppose you simply do not declare a return type until you upgrade your PHP version. \$\endgroup\$ May 26 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
    May 26 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
    May 26 at 16:17

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.