5
\$\begingroup\$

Some methods of this library require some improvements (see this description):

  1. In the constructor I am trying to create the possibility that the user modifies the colors of the output. I think it can improve but I do not know if there is any standard or good practices in this regard, or if there is any way to optimize this class.

  2. The method getIndent uses the analysis of the variable to determine the indentation of the output using the function array_walk_recursive. I don't have much knowledge about the handling of arrays, therefore I used this method because I found it simple. I don't know if it is the best to fulfil the purpose of the getIndent method.

  3. The AnalVariable method uses the abstraction of the pretty function. It was the only example I found in which it allowed me to have an output similar to that of var_dump or var_export. Some other developers suggested to me to use reflection but I did not find an example related to the use / purpose that I want to give it.

    I also have an analysis cluster for spacing using ternary analysis. I would like someone with more experience to check, looking for possible errors or improvements. With the minimal tests that I have carried out I have not obtained any error.

  4. The EvalVariable method is my favorite; This method is responsible for analyzing the data of the passed variable to determine the type of data and return it, taking into account that the returned code / value is usable. But I have not been able to recreate the objects or resources to make them usable, let's see an example :

    Example array we have a node as follows:

    'resource' => curl_init()
    

    but in the output tube to put:

    'resource' => resource
    

    This happens because the data type does not say that it comes from a curl_init(); what I was hoping to do, which I have failed to do, is show the output:

    'resource' => curl_init()
    
  5. Another thing that I also think can negatively impact the performance of the library is the analysis done by the EvalVariable method since it is a set of if and verifications. I think it can be improved but I don't know where to start or what things I should change.

  6. I have also managed to improve the output a bit by adding or enriching it with a comment where I describe a more in-depth analysis.

    // object (stdClass) # 2 (2).
    // object (FooBar) # 3 (0).
    // resource (6) of type (curl).
    
  7. The method HighlightCode is in charge of fixing the screen output. In this case, I use the highlight_string method in conjunction with other concatenations to take it to an HTML. But I don't know how I can recreate this for the CLI, which I would like to implement.

  8. Another important point for me is to evaluate if in general the Script / library complies with the active PSR standards. I use the phpStorm IDE but it does not detect an error of this type.

My goal is to improve this script. I have listed the main points, and I await comments on them. Although it is not the main objective, I am also open to hear opinions based on documented and exemplified improvements.

Class/library Description:

[BOH] Basic Output Handler for PHP

Acronym: [BOH].
Name: Basic Output Handler.
Dependencies: Stand Alone / PHP v7.4.

What does [BOH] do?

[BOH] is a very simple PHP [output handler] implementation that show Human readable information instead of using the default PHP options:

  • var_dump() - Displays information about a variable.
  • print_r() - Print human-readable information about a variable.
  • debug_zval_dump() - Outputs a string representing an internal value of zend.
  • var_export() - Print or return a string representation of a parseable variable.

This means that all the data passed is presented to the developer according to the chosen parameters. It also means that the displayed data can be directly reused as code. Comments are also generated for each value that briefly explains the type of data

Why use [BOH]?

Developers need the ability to decide how their code behaves when data needs to be checked. The native PHP Methods provide a range of information that is not reusable by the developer or may even require more work to get the correct output for data verification.

This library handles data output proven to be extremely effective. [BOH] is a standalone implementation that can be used for any project and does not require a third-party library or software.

Help to improve [BOH]?

If you want to collaborate with the development of the library; You can express your ideas or report any situation related to this in: the issues section of the repository on github

My code

<?php

namespace IcarosNet\BOHBasicOutputHandler;

if (!version_compare(phpversion(), '7.4', '>=')) {
    die('IcarosNet\BOHBasicOutputHandler requires PHP ver. 7.4 or higher');
}

if (!defined('ENVIRONMENT_OUTPUT_HANDLER')) {
    define('ENVIRONMENT_OUTPUT_HANDLER', (IsCommandLineInterface() ? 'cli' : 'web'));
}

class OutputHandler
{
    public string $background = '';
    public string $themeused;
    public string $defenv = '';
    public array $colorcli = [
        "comment"    => '',
        "constant"   => '',
        "function"   => '',
        "keyword"    => '',
        "magic"      => '',
        "string"     => '',
        "tag"        => '',
        "variable"   => '',
        "html"       => '',
        ""           => "%s",
        "background" => '',
    ];

    public function __construct($theme = 'default')
    {
        $this->Theme($theme);
        $this->defenv = ENVIRONMENT_OUTPUT_HANDLER;
    }

    public function __destruct()
    {
        $this->ResetHighlight();
    }

    //Theme Code and Highlight

    public function ResetHighlight()
    {
        ini_set("highlight.comment", "#FF9900");
        ini_set("highlight.default", "#0000BB");
        ini_set("highlight.html", "#000000");
        ini_set("highlight.keyword", "#007700; font-weight: bold");
        ini_set("highlight.string", "#DD0000");
    }

    public function Theme(string $theme = 'default')
    {
        $this->themeused = $theme;
        switch ($theme) {
            case 'x-space':
                $color            = ['043,128,041', '099,099,099', '128,128,128', '072,094,187', '221,079,079', '000,000,000'];
                $this->background = '000000';
                break;
            case 'mauro-dark':
                $color            = ['187,134,252', '250,250,250', '003,218,197', '255,204,255', '207,102,121', '018,018,018'];
                $this->background = '121212';
                break;
            case 'natural-flow':
                $color            = ['145,155,152', '30,156,107', '003,218,197', '006,156,004', '139,156,51', '004,041,003'];
                $this->background = '042903';
                break;
            case 'monokai':
                $color            = ['117,113,94', '255,255,255', '102,217,239', '249,038,114', '230,219,116', "039,040,034"];
                $this->background = '272822';
                break;
            default:
                $color            = ['255,095,000', '000,000,255', '000,000,000', '000,175,000', '255,000,000', '255,255,255'];
                $this->background = 'ffffff';
                break;
        }
        $this->UpdateInitSetHighlight($color);
        $this->UpdateInitSetHighlightCli($color);
    }

    private function UpdateInitSetHighlight($color)
    {
        ini_set("highlight.comment", 'rgb(' . $color[0] . '); background-color: #' . $this->background);
        ini_set("highlight.default", 'rgb(' . $color[1] . '); background-color: #' . $this->background);
        ini_set("highlight.html", 'rgb(' . $color[2] . '); background-color: #' . $this->background);
        ini_set("highlight.keyword", 'rgb(' . $color[3] . "); font-weight: bold; background-color: #" . $this->background);
        ini_set("highlight.string", 'rgb(' . $color[4] . ');background-color: #' . $this->background);
    }

    private function UpdateInitSetHighlightCli($color)
    {
        $this->colorcli['comment']    = "\033[38;2;" . $this->RGBforCLI($color[0]) . "m%s\033[0m";
        $this->colorcli['constant']   = "\033[38;2;" . $this->RGBforCLI($color[4]) . "m%s\033[0m";
        $this->colorcli['function']   = "\033[38;2;" . $this->RGBforCLI($color[1]) . "m%s\033[0m";
        $this->colorcli['keyword']    = "\033[38;2;" . $this->RGBforCLI($color[3]) . "m%s\033[0m";
        $this->colorcli['magic']      = "\033[38;2;" . $this->RGBforCLI($color[1]) . "m%s\033[0m";
        $this->colorcli['string']     = "\033[38;2;" . $this->RGBforCLI($color[4]) . "m%s\033[0m";
        $this->colorcli['tag']        = "\033[38;2;" . $this->RGBforCLI($color[1]) . "m%s\033[0m";
        $this->colorcli['variable']   = "\033[38;2;" . $this->RGBforCLI($color[3]) . "m%s\033[0m";
        $this->colorcli['html']       = "\033[38;2;" . $this->RGBforCLI($color[2]) . "m%s\033[0m";
        $this->colorcli['background'] = "\033[48;2;" . $this->RGBforCLI($color[5]) . "m";
    }

    private function RGBforCLI($color)
    {
        return str_replace(',', ';', $color);
    }

    private function HighlightCode(string $string): string
    {
        return highlight_string("<?php \n#output of Variable:" . str_repeat(' ', 10)
            . '*****| Theme Used: ' . $this->themeused . " |*****\n" . $string . "\n?>", true);
    }

    private function HighlightCodeCli(string $string): string
    {
        $bg     = $this->colorcli['background'];
        $string = '<?php' . PHP_EOL . $string . PHP_EOL . '?>';
        $string = $this->CoverforBackground($string);
        $COLORS = $this->colorcli;
        $TOKENS = [
            T_AS                       => "as",
            T_CLOSE_TAG                => "tag",
            T_COMMENT                  => "comment",
            T_CONCAT_EQUAL             => "",
            T_CONSTANT_ENCAPSED_STRING => "string",
            T_CONTINUE                 => "keyword",
            T_DOUBLE_ARROW             => "variable",
            T_ECHO                     => "keyword",
            T_ELSE                     => "keyword",
            T_FILE                     => "magic",
            T_FOREACH                  => "keyword",
            T_FUNCTION                 => "keyword",
            T_IF                       => "keyword",
            T_IS_EQUAL                 => "",
            T_ISSET                    => "keyword",
            T_LIST                     => "keyword",
            T_OPEN_TAG                 => "tag",
            T_RETURN                   => "keyword",
            T_STATIC                   => "keyword",
            T_VARIABLE                 => "variable",
            T_WHITESPACE               => "",
            T_LNUMBER                  => "function",
            T_DNUMBER                  => "function",
            T_OBJECT_CAST              => "variable",
            T_STRING                   => "function",
            T_INLINE_HTML              => "",
        ];
        $output = "";
        foreach (token_get_all($string) as $token) {
            if (is_string($token)) {
                $output .= $bg . $token . "\033[0m";
                continue;
            }
            list($t, $str) = $token;
            if ($t == T_STRING) {
                if (function_exists($str)) {
                    $output .= $bg . sprintf($COLORS["function"], $str) . "\033[0m";
                } else {
                    if (defined($str)) {
                        $output .= $bg . sprintf($COLORS["function"], $str) . "\033[0m";
                    } else {
                        $output .= $bg . sprintf($COLORS["function"], $str) . "\033[0m";
                    }
                }
            } else {
                if (isset($TOKENS[$t])) {
                    $output .= $bg . sprintf($COLORS[$TOKENS[$t]], $str) . "\033[0m";
                } else {
                    $output .= $bg . sprintf("<%s '%s'>", token_name($t), $str) . "\033[0m";
                }
            }
        }
        return $output;
    }


    private function CoverforBackground(string $string): string
    {
        $info      = shell_exec('MODE 2> null') ?? shell_exec('tput cols');
        $widthreal = 80;
        if (strlen($info) > 5) {
            preg_match('/CON.*:(\n[^|]+?){3}(?<cols>\d+)/', $info, $match);
            $widthreal = $match['cols'] ?? 80;
        }
        $width     = (int) $widthreal - 10;
        $stringarr = preg_split('/\r\n|\r|\n/', rtrim($string));
        $numline   = count($stringarr);
        $maxlen    = max(array_map(function ($el) {
            return mb_strlen($el);
        }, $stringarr));
        $longest   = ($maxlen > $width ? $maxlen : $width);
        if ($maxlen > $widthreal) {
            echo 'Oops, your terminal window is not wide enough to display the information correctly.' . PHP_EOL .
                'If you can increase the amount of characters per line (' . ($maxlen + 10) . ') it would work correctly.';
            exit;
        }
        $string = '';
        $count  = 1;
        foreach ($stringarr as $key => $line) {
            $lenline = mb_strlen($line);
            $string  .= $line . str_repeat(' ', $longest - $lenline) . ($count < $numline ? PHP_EOL : '');
            $count++;
        }
        return $string;
    }

    private function ApplyCss(string $string): string
    {
        $bg    = '#' . $this->background;
        $class = mt_rand();
        return '<style>.outputhandler-' . $class . '{background-color: ' . $bg . '; padding: 8px;border-radius: 8px;}</style>
                    <div class="outputhandler-' . $class . '">' . $string . '</div>';
    }

    //core Analysis or OuputHandler

    private function CheckEnv($env): string
    {
        $iscli = IsCommandLineInterface();
        $env   = ($env == null ? $this->defenv : $env);
        if ($iscli && $env == 'wb') {
            echo 'error: you are trying to run output() method from CLI and it is not supported, use OutputCli() or AdvanceOutput() with CLI argument  method instead.';
            exit;
        } elseif (!$iscli && $env == 'cli') {
            echo 'error: you are trying to run OutputCli() method from web browser and it is not supported, use Output() or AdvanceOutput() with HTML argument method instead.';
            exit;
        }
        return $env;
    }

    public function Output($var, $env = null, $retrive = false)
    {
        $env = $this->CheckEnv($env);
        if ($env == 'web') {
            $string = $this->OutputWb($var, $retrive);
        } elseif ($env == 'cli') {
            $string = $this->OutputCli($var, $retrive);
        } else {
            $string = $this->OutputWb($var, $retrive);
        }
        if ($retrive) {
            return $string;
        }
    }

    public function OutputWb($var, $retrive = false)
    {
        $indents = $this->GetIndent($var);
        $string  = $this->GetString($var, $indents);
        $string  = $this->HighlightCode($string);
        $string  = $this->ApplyCss($string);
        $this->ResetHighlight();
        return ($retrive ? $string : $this->OutView($string));
    }

    public function OutputCli($var, $retrive = false)
    {
        $indents = $this->GetIndent($var);
        $string  = $this->GetString($var, $indents);
        $string  = $this->HighlightCodeCli($string);
        $this->ResetHighlight();
        return ($retrive ? $string : $this->OutView($string));
    }

    private function GetIndent($var): array
    {
        $data    = $var;
        $indents = ['key' => 0, 'val' => 0];
        if (is_array($data) || is_object($data)) {
            array_walk_recursive($data, function (&$value) {
                $value = is_object($value) ? (array) $value : $value;
            });
            $deep = ($this->CalcDeepArray($data) + 1) * 4;
            array_walk_recursive($data, function ($value, $key) use (&$indents) {
                $indents['key'] = ($indents['key'] >= mb_strlen($key)) ? $indents['key'] : mb_strlen($key);
                if (!is_array($value) && !is_object($value) && !is_resource($value)) {
                    $indents['val'] = ($indents['val'] >= mb_strlen($value)) ? $indents['val'] : mb_strlen($value);
                }
            }, $indents);
            $indents['key'] += $deep;
            $indents['val'] += $deep / 2;
        } else {
            $indents = ['key' => mb_strlen('variable'), 'val' => mb_strlen($data)];
        }
        return $indents;
    }

    private function CalcDeepArray(array $array): int
    {
        $max_depth = 0;
        foreach ($array as $key => $value) {
            if (is_array($value)) {
                $depth = $this->CalcDeepArray($value) + 1;
                if ($depth > $max_depth) {
                    $max_depth = $depth;
                }
            }
        }
        return $max_depth;
    }

    private function GetString($var, array $indents): string
    {
        return $this->AnalysisVariable('variable', $var, $indents);
    }

    private function AnalysisVariable(string $varname, $var, array $indents): string
    {
        $pretty      = function ($indents, $varlentitle, $v = '', $c = " ", $in = 0, $k = null) use (&$pretty) {
            $r = '';
            if (in_array(gettype($v), array('object', 'array'))) {
                $lenname = mb_strlen("'$k'");
                $lenkeys = $indents['key'] - $in - $lenname;
                if ($lenkeys < 0) {
                    $lenkeys = 0;
                }
                $eval   = $this->EvaluateVariable($v);
                $v      = (array) $v;
                $lenkey = $indents['val'] - mb_strlen($eval['val']) + 1;
                if (empty($v)) {
                    $r .= ($in != 0 ? str_repeat($c, $in) : '') . (is_null($k) ? '' : "'$k'"
                            . str_repeat($c, $lenkeys) . "=> " . $eval['val'] . "[],"
                            . str_repeat(" ", $lenkey - 6) . "// "
                            . $eval['desc']) . (empty($v) ? '' : PHP_EOL);
                } else {
                    $r .= ($in != 0 ? str_repeat($c, $in) : '') . (is_null($k) ? '' : "'$k'"
                            . str_repeat($c, $lenkeys) . "=> " . $eval['val'] . "["
                            . str_repeat(" ", $lenkey - 4) . "// "
                            . $eval['desc']) . (empty($v) ? '' : PHP_EOL);
                    foreach ($v as $sk => $vl) {
                        $r .= $pretty($indents, $varlentitle, $vl, $c, $in + 4, $sk) . PHP_EOL;
                    }
                    $r .= (empty($v) ? '],' : ($in != 0 ? str_repeat($c, $in / 2) : '')
                        . (is_null($v) ? '' : str_repeat($c, $in / 2) . "],"));
                }
            } else {
                $lenkey = $indents['key'] - mb_strlen("'$k'") - $in;
                if ($lenkey < 0) {
                    $lenkey = 0;
                }
                $eval   = $this->EvaluateVariable($v);
                $lenval = $indents['val'] - (mb_strlen("'" . $eval['val'] . "'"));
                if ($lenval < 0) {
                    $lenval = 0;
                }
                $r .= ($in != -1 ? str_repeat($c, $in) : '') . (is_null($k) ? '' : "'$k'"
                        . str_repeat($c, $lenkey) . '=> ') . $eval['val']
                    . str_repeat(" ", $lenval) . '// ' . $eval['desc'];
            }
            return $r;
        };
        $varlentitle = mb_strlen('$' . $varname);
        if (in_array(gettype($var), array('object', 'array'))) {
            return '$' . $varname . str_repeat(" ", ($indents['key'] - $varlentitle)) . '= ['
                . str_repeat(" ", $indents['val'] - 2) . '// main array node'
                . rtrim($pretty($indents, $varlentitle, $var), ',') . ';';
        } else {
            $eval = $this->EvaluateVariable($var);
            return '$' . $varname . str_repeat(" ", $indents['key']) . '=' . $eval['val'] . ';'
                . str_repeat(" ", $indents['val'] - 1) . '// ' . $eval['desc'];
        }
    }

    public function EvaluateVariable($var): array
    {
        if (null === $var || 'null' === $var || 'NULL' === $var) {
            if (is_string($var)) {
                return ['val' => "'null'", 'desc' => 'null value string.'];
            } else {
                return ['val' => 'null', 'desc' => 'null value.'];
            }
        }

        if (is_array($var)) {
            return ['val' => "", 'desc' => 'array node.'];
        }

        if (in_array($var, ["true", "false", true, false], true)) {
            if (is_string($var)) {
                return ['val' => "'" . $var . "'", 'desc' => 'string value boolean ' . $var . '.'];
            } else {
                return ['val' => ($var ? 'true' : 'false'), 'desc' => 'boolean value ' . ($var ? 'true' : 'false') . '.'];
            }
        }

        ob_start();
        var_dump($var);
        $string = ob_get_clean();
        if (is_object($var)) {
            $string = explode('{', $string);
            return ['val' => '(object) ', 'desc' => rtrim($string[0]) . '.'];
        }
        unset($string);

        if ((int) $var == $var && is_numeric($var)) {
            if (is_string($var)) {
                return ['val' => "'" . $var . "'", 'desc' => '(' . mb_strlen($var) . ') integer value string.'];
            } else {
                return ['val' => $var, 'desc' => '(' . mb_strlen($var) . ') integer value.'];
            }
        }

        if ((float) $var == $var && is_numeric($var)) {
            if (is_string($var)) {
                return ['val' => "'" . $var . "'", 'desc' => '(' . mb_strlen($var) . ') float value string.'];
            } else {
                return ['val' => $var, 'desc' => '(' . mb_strlen($var) . ') float value.'];
            }
        }

        ob_start();
        var_dump($var);
        $string = ob_get_clean();
        if (mb_strpos($string, 'resource') !== false) {
            return ['val' => 'resource', 'desc' => rtrim($string) . '.'];
        } elseif (mb_strpos($string, 'of type ') !== false) {
            return ['val' => 'resource', 'desc' => rtrim($string) . '.'];
        }
        unset($string);

        if (mb_strpos($var, ' ') !== false && mb_strpos($var, ':') !== false && mb_strpos($var, '-') !== false) {
            $datetime = explode(" ", $var);
            $validate = 0;
            foreach ($datetime as $value) {
                if ($this->ValidateDate($value)) {
                    $validate++;
                }
            }
            if ($validate >= 2) {
                return ['val' => "'" . $var . "'", 'desc' => '(' . mb_strlen($var) . ') string value datetime.'];
            }
        }

        if ($this->ValidateDate($var) && mb_strpos($var, ':') !== false) {
            return ['val' => "'" . $var . "'", 'desc' => '(' . mb_strlen($var) . ') string value time.'];
        }

        if ($this->ValidateDate($var) && mb_strlen($var) >= 8 && mb_strpos($var, '-') !== false) {
            return ['val' => "'" . $var . "'", 'desc' => '(' . mb_strlen($var) . ') string value date.'];
        }

        if ($this->ValidateDate($var) && mb_strlen($var) >= 8 && mb_strpos($var, '-') !== false) {
            return ['val' => "'" . $var . "'", 'desc' => '(' . mb_strlen($var) . ') string value date.'];
        }

        if (is_string($var)) {
            $arr           = $this->StrSplitUnicode($var);
            $currencylist  = [
                '¤', '$', '¢', '£', '¥', '₣', '₤', '₧', '€', '₹', '₩', '₴',
                '₯', '₮', '₰', '₲', '₱', '₳', '₵', '₭', '₪', '₫', '₠', '₡', '₢', '₥', '₦',
                '₨', '₶', '₷', '₸', '₺', '₻', '₼', '₽', '₾', '₿'
            ];
            $currencycheck = [];
            foreach ($arr as $char) {
                if (in_array($char, $currencylist, true)) {
                    $currencycheck[] = $char;
                }
            }
            if (!empty($currencycheck)) {
                return [
                    'val' => "'" . $var . "'", 'desc' => 'string/amount value related to currency ('
                        . implode(',', $currencycheck) . ').'
                ];
            }
        }

        if (is_string($var)) {
            return ['val' => "'" . $var . "'", 'desc' => 'string value of ' . mb_strlen($var) . ' character.'];
        }

        return ['val' => 'unknow', 'desc' => 'unknow'];
    }

    private function ValidateDate(string $date): bool
    {
        return (strtotime($date) !== false);
    }

    private function StrSplitUnicode(string $str, $length = 1): array
    {
        $tmp = preg_split('~~u', $str, -1, PREG_SPLIT_NO_EMPTY);
        if ($length > 1) {
            $chunks = array_chunk($tmp, $length);
            foreach ($chunks as $i => $chunk) {
                $chunks[$i] = join('', (array) $chunk);
            }
            $tmp = $chunks;
        }
        return $tmp;
    }

    private function OutView(string $string)
    {
        echo $string;
    }
}

function IsCommandLineInterface(): bool
{
    return (php_sapi_name() === 'cli');
}

Example Usage

<?php

use \IcarosNet\BOHBasicOutputHandler as Output;

require __DIR__ . '\..\vendor\autoload.php';

/**
 * FooBar is an example class.
 */
class FooBar
{
    function foo_function()
    {
        return "Hello World!";
    }
}

$var_class = new FooBar;

$example_single = 'Hello World';

$example_array = [//1
    'null'         => null,
    'null_text'    => 'null',
    'integer'      => 10,
    'integer_text' => '10',
    'float'        => 20.35,
    'float_text'   => '20.35',
    'string'       => 'Hello World',
    'date_1'       => '2021-01-17',
    'date_2'       => '2021-Jan-17',
    'hour_1'       => '6:31:00 AM',
    'hour_2'       => '17:31:00',
    'datetime_1'   => '2021-01-17 17:31:00',
    'datetime_2'   => '2021-Jan-17 6:31:00 AM',
    'datetime_3'   => '2021-01-17 6:31:00 AM',
    'datetime_4'   => '2021-Jan-17 17:31:00',
    'currency_1'   => '1.45$',
    'currency_2'   => 'db£ 1.45 ₹',
    'array'        => [//2
        'boolean_true'       => true,
        'boolean_false'      => false,
        'boolean_true_text'  => 'true',
        'boolean_false_text' => 'false',
        'object'             => (object) [//3
            'key_index_most' => 'Hello Wolrd',
            'joder'          => [//4
                'prueba' => 'prueba',
            ]
        ],
        'nested'             => [                                               // deep = 3 no cuenta ya existe
            'other_obj' => (object) [                               // deep = 4 no cuenta ya existe
                'apple',
                'banana',
                'coconut',
            ],
        ],
    ],
    'objects_list' => [
        'object_empty' => (object) [],
        'class'        => $var_class,
        'resource'     => curl_init(),
    ],
];

$output = new Output\OutputHandler();
$output->Theme('monokai');
$output->Output($example_array);

// Other Examples:

Example Output:

Please keep in mind that this output was captured in the browser, although it has a code format, this is one of the functionalities of the class. the format is not possible to replicate in SE sites, so I add the images.

  • default theme enter image description here

  • monokai theme monokai theme

  • natural-flow enter image description here

\$\endgroup\$
1
2
\$\begingroup\$

Standards

    private function AnalysisVariable(string $varname, $var, array $indents): string

So there are a couple problems here. In the text of your question, you refer to this as AnalVariable. That's a particularly bad name as anal is an English word that is somewhat off-color. Fortunately changed here.

PSR-1 says that method names should be camelCased. StudlyCaps are used for class names.

In general, method names should be verbs. Analysis is a noun. The verb form is analyze.

So this should be

    private function analyzeVariable(string $varname, $var, array $indents): string

You might find that people would find this method more useful if it were accessible when the class was extended. I.e. if it were protected instead of private.

Efficiency

        ob_start();
        var_dump($var);
        $string = ob_get_clean();
        if (is_object($var)) {
            $string = explode('{', $string);
            return ['val' => '(object) ', 'desc' => rtrim($string[0]) . '.'];
        }
        unset($string);

This seems weird to me. If $var is not an object, you do an ob_start and ob_get_clean only to immediately throw away the result. Why not reorder to

        if (is_object($var)) {
            ob_start();
            var_dump($var);
            return ['val' => '(object) ', 'desc' => rtrim(explode('{', ob_get_clean(), 2)[0]) . '.'];
        }

Now, there is no need to unset $string, as it is never created.

If $var is not an object, we don't do the output buffering.

I think that string is a bad name for an array. However, it's hard to see what it should be named in this case. Particularly as you are only using the first element from the array.

I also only split on the first brace, rather than on every one that may be present in the string.

\$\endgroup\$
3
  • \$\begingroup\$ reset only support variable passed by reference not expresions or function XD \$\endgroup\$ – Francisco Núñez-Todo Poderoso Feb 26 at 14:57
  • \$\begingroup\$ i.imgur.com/nG4IW6l.png \$\endgroup\$ – Francisco Núñez-Todo Poderoso Feb 26 at 15:00
  • 1
    \$\begingroup\$ This revised version will work, although it would probably be more readable to make an intermediate variable if you can come up with a reasonable name. \$\endgroup\$ – mdfst13 Feb 26 at 20:15
1
\$\begingroup\$

Initial feedback

This code looks okay, though has quite a bit of room for improvement - see this suggestions below. I like how the short array syntax is used in many places, as well as type hinting of parameters and return types for methods.

Some methods are a bit lengthy and a bit of an eyesore - e.g. the method EvaluateVariable- its more than 100 lines of code! Consider breaking that up into smaller methods to reduce redundancy and allow for better testing. Speaking of which - do you have any unit tests or plan to make some? If I consider using a library I look to see if it has unit tests and whether they are passing.

The method AnalysisVariable() is also very long. The function stored in $pretty does not need to be an anonymous function - it can be a method instead.

If I was to use a library like this then I would prefer not to have to instantiate an object- even once. A singleton pattern could be used if instances are necessary but as a Laravel user who is familiar with the illuminate logger methods I like how most output methods are static so I don’t have to have an instance of a helper object in order to use them.

Bugs

warning on call to str_repeat()

I tried passing a simple array to the Output method:

$example_array = ['a' => 1, 'b'=>2];

$output->Output($example_array);


That yielded a warning before the output:

Warning: str_repeat(): Second argument has to be greater than or equal to 0 in outputtest.php on line 360

It appears you may have fixed this with the code in the repository, given that you updated the example code there to include this sample array.

CLI column width pattern match

I tried installing the library via Composer on my mac. No matter how wide I make my terminal it always shows the Oops message. Apparently MODE 2> null yields nothing while tput cols yields a single integer - e.g. 176 so the regular expression starting with CON.*: won't match anything.

Suggestions

Don’t repeat yourself

In the method UpdateInitSetHighlightCli() it appears that the only thing that changes in each line except the last is the key to set and the key to index - those could be put into an associative array to map keys and then use a foreach loop to set values accordingly.

 $this->colorcli['comment']    = "\033[38;2;" . $this->RGBforCLI($color[0]) . "m%s\033[0m";
  $this->colorcli['constant']   = "\033[38;2;" . $this->RGBforCLI($color[4]) . "m%s\033[0m";
// and so on

Could be simplified:

const CLI_KEY_COLOR = [
    'comment' => 0,
    'constant' => 4,     
    // and do on  
];

then loop through those:

foreach (self::CLI_KEY_COLOR  as $key => $colorIndex) {
    $this->colorcli[$key]    = "\033[38;2;" . $this->RGBforCLI($color[$colorIndex]) . "m%s\033[0m";
}
//the exception:
$this->colorcli['background'] = "\033[48;2;" . $this->RGBforCLI($color[5]) . "m";

Constants

Any data with a name in all caps - e.g. $TOKENS is a convention of a constant. For proper constant declaration, use the const keyword - which would make it not a variable.

It might be wise to consider pulling the values used in the Theme method, as well as other data that doesn't get modified- like $currencylist - out into constants - e.g. for the colors and background colors. That way if any of those values needed to be updated, you wouldn't have to go down into the method, but rather at the constant definition. Also, it would allow for a different way to set those values - e.g. instead of using a switch statement, check if the value of $theme exists as a key in an array of values in a constant - if so, use the value at that index.

You might also consider defining constants for strings like wb and cli to avoid mistakes caused by typos.

I don’t see much point in defining ENVIRONMENT_OUTPUT_HANDLER since it only appears to be used in the constructor. The constructor could simply call the function to set the value accordingly.

PHP version check

if (!version_compare(phpversion(), '7.4', '>=')) {

Since the was added to the post, I can’t help but notice this line uses the functions to compare versions

Instead of calling those two functions, use the PHP_VERSION_* constants to reduce the number of function calls, though you might have to ensure those are defined first - if not then you’d know the version was likely prior to 5.2.7.

Method name convention

I know mdfst13 already mentioned this but I planned to mention it anyway. While it is not required, it is good to follow the PSRs for readability and maintainbility. PSR-1 recommends the following convention for method names:

Method names MUST be declared in camelCase.

character replacement

In some cases using strtr() could be faster than str_replace() but you should test on the input your code normally uses to see what may be best- the differences may be negligible.

docblocks

I happened to see that you updated your code, which needed to be rolled back since an answer had already been submitted and the code shouldn't be changed after that happens. In that revision you added dockblocks - I was intending to mention those. Good work adding those. You can post a follow-up question for the code that contains those.

Null coalescing operator

Since the code requires PHP version 7.4 or newer, the null coalescing operator can be used. For example, instead of this line in the method CheckEnv():

$env   = ($env == null ? $this->defenv : $env);
$env   = 

Simplify it to this:

$env ?? $this->defenv;

else after return

In that method EvaluateVariable() I see this (indentation decreased for the sake of readability):

if (null === $var || 'null' === $var || 'NULL' === $var) {
    if (is_string($var)) {
        return ['val' => "'null'", 'desc' => 'null value string.'];
    } else {
        return ['val' => 'null', 'desc' => 'null value.'];
    }
}

There is no need to have an else block when the previous block(s) contain a return statement.

Anonymous function in call to array_map()

In the method CoverforBackground() (which is apparently renamed to adjusterSpaceLine()) there is this code:

    $maxlen    = max(array_map(function ($el) {
        return mb_strlen($el);
    }, $stringarr));

The extra anonymous function can be removed from the call to array_map() by passing the function name in a string literal:

$maxlen    = max(array_map('mb_strlen', $stringarr));

If you still wanted an anonymous function there, then an arrow function could be used, given that PHP 7.4 is required for the code:

$maxlen    = max(array_map(fn($el) => mb_strlen($el), $stringarr));
\$\endgroup\$
6
  • \$\begingroup\$ thank for your review. \$\endgroup\$ – Francisco Núñez-Todo Poderoso Feb 26 at 21:13
  • \$\begingroup\$ hello the bug you find is based on the code of this question or you have installed the lasted version with composer like the documentation say? \$\endgroup\$ – Francisco Núñez-Todo Poderoso Feb 26 at 21:52
  • \$\begingroup\$ I don't know how to do unit tests yet, it's in my long-term plans. \$\endgroup\$ – Francisco Núñez-Todo Poderoso Feb 26 at 21:54
  • \$\begingroup\$ i have 2 option a null estatemente value and a null statement like string value... becouse it i need else block, but i can minimifid it \$\endgroup\$ – Francisco Núñez-Todo Poderoso Feb 26 at 22:51
  • 1
    \$\begingroup\$ Thank you I will continue to make new improvements throughout tonight. Regards. \$\endgroup\$ – Francisco Núñez-Todo Poderoso Mar 1 at 22:20
0
+100
\$\begingroup\$

FYI - I'm a bit rusty with PHP and not a PHP expert in anyways.

TL;DR

  • Be strict on coding style and standard.
  • Clean the class responsibility, implement (at least) Single Responsibility Principle.
  • Make smaller functions.
  • Write Unit test.

Retrospect

Use Strict!

This generated a lot of errors that was not there before.

<?php declare(strict_types=1);

Access Specifiers

Private, Protected and Public are used, which is great to see but I think there has NOT been put a lot of thought for doing it.

Class

In general, class OutputHandler is too large, more than 400 lines.The standard rule would be maximum lines of a Class is around 100.

It has too many functionalities which could be separated out in another class of just utility functions which can be called directly.

Like IsCommandLineInterface() we can move other common usage functions to a separate file so that our class could be cleaner. Or if are moving with a pure Object Oriented approach then probably make another class.

I think the best way would be to make separate classes for CLI and Web version. We can have a OutputHandler Abstract base class with some similary functionalities which can be extended by class CliOutputHandler and WebOutputHandler child classes to implement needed features dependently.

Furthermore,I can see the validation process could be move out to a separate class just with the validation functionality.

And some more classes are possible for other small utility purposes which could be used just as a function but as I have mentioned let go complete OO.

Constructor

We might want to do pass example array kind of input variable here and do the validation in the constructor. It would be a good practice.

public function __construct($theme = 'default')
{
  $this->getTheme($theme);
  // This is variable is not used so remove. 
  // $this->defenv = ENVIRONMENT_OUTPUT_HANDLER;
}

Function getTheme()

public function getTheme(string $theme = 'default'): void
{
    if (isset(self::THEMESLIST[$theme])) {
        $color = self::THEMESLIST[$theme];
        $this->themeused = $theme;
    
    } else {
        $color = self::THEMESLIST['default'];
        $this->themeused = 'default';
    }

    IsCommandLineInterface() ? 
        $this->setHighlightThemeCli($color)
        : $this->setHighlightTheme($color);
    
}       

Function output()

I can see the goodness of strongly type being implemented in few functions. What I would love to see is that, that standard being maintained in this function signature as well.

Minor modification is done in the implementation.

public function output($var, $env = null, $retrieve = false)
{
    
    $indents = $this->getIndent($var);
    $string  = $this->analyzeVariable($var, $indents);

    if (IsCommandLineInterface() ) {
        $string  = $this->highlightCodeCli($string);

    } else {
        $string  = $this->highlightCode($string);
    }

    $string  = $this->applyCss($string);
    $this->resetHighlight();

    return ($retrieve ? $string : $this->outView($string));
}

Function getIndent()

One of function I am not happy seeing as I am not able to understand what's the purpose of the function and the way it has been implemented.

We are returning indents array with [key => value] but we are doing trying to find the length of the array and objects, this is not done using mb_strlen.

private function getIndent($data): array
{
    $indents = ['key' => 0, 'val' => 0];

    if ( is_array($data) || is_object($data) ) {
        
        $data = is_object($data) ? (array) $data : $data;
        
        // This literally does not make any sense.
        // Why are we looping and assigning value to 
        // the same variable $value? And the $value is 
        // used in the local scope.
        // array_walk_recursive($data, function (&$value) {
        //     $value = is_object($value) ? (array) $value : $value;
        // });
        
        // THIS WILL GIVE ERROR!
        // var_dump($value);

        $deep = ($this->calcDeepArray($data) + 1) * 4;
        
        array_walk_recursive($data, function ($value, $key) use (&$indents) {

            if (mb_strpos(strval($key), chr(0)) !== false) {
                $key = str_replace(chr(0), "'::'", $key);
                $key = substr($key, 4);
            }
            
            // It does not make sense to use mb_strlen for
            // array and objects.
            if (!is_array($value) 
                && !is_object($value) 
                && !is_resource($value) 
                && !is_null($value)) {
                $indents['val'] = ($indents['val'] >= mb_strlen(strval($value))) 
                    ? $indents['val'] : mb_strlen(strval($value));

                $indents['key'] = ( $indents['key'] >= mb_strlen(strval($key)) ) 
                    ? $indents['key'] : mb_strlen(strval($key));
            }
            

        }, $indents);

        $indents['key'] += $deep;
        $indents['val'] += $deep / 2;

    } else {
        $indents = ['key' => mb_strlen('variable'), 'val' => mb_strlen($data)];
    }

    return $indents;
}

function analyzeVariable

This is a bit messy. Let try to remove the recursion and check it other simple way. I believe the purpose of the function is to validate the variables in the input. A good way to design a API is to stick to some form of input standards. I think that usage of JSON format would be good as it helps to validate the schema easily. Here is an example of JSON Schema validator:

https://github.com/opis/json-schema

But it is not mandatory to do it.

protected function analyzeVariable($data, array $indents): string { $varname = 'variable';

// We are using anonymous function to do some 
// analysis but I believe there is a better 
// way to do it, even without the use of recursion!
// I am not sure what are the validation criteria
// as the function is too hard to understand what's
// going on.
$pretty      = function ($indents, $varlentitle, $v = '', $c = " ", $in = 0, $k = null) use (&$pretty) {
    $r = '';
    
    // Previously, checking of object and array was done
    // like this:
    //   if ( is_array($data) || is_object($data) ) {
    // Why are we chaning the way we check?
    if (in_array(gettype($v), ['object', 'array'])) {
        
        if (!is_null($k)) {
            if (mb_strpos($k, chr(0)) !== false) {
                $k = str_replace(chr(0), "'::'", $k);
                $k = substr($k, 4);
            }
        }
        $lenname = mb_strlen("'$k'");
        $lenkeys = $indents['key'] - $in - $lenname;

        if ($lenkeys < 0) {
            $lenkeys = 0;
        }

        $eval   = $this->evaluateVariable($v);

        $v      = (array) $v;
        $lenkey = $indents['val'] - mb_strlen($eval['val']) + 1;
        
        if (empty($v)) {
            $r .= ($in != 0 ? str_repeat($c, $in) : '') . (is_null($k) ? '' : "'$k'"
                    . str_repeat($c, $lenkeys) . "=> " . $eval['val'] . "[],"
                    . str_repeat(" ", $lenkey - 6) . "// "
                    . $eval['desc']) . (empty($v) ? '' : PHP_EOL);
        } else {
            $r .= ($in != 0 ? str_repeat($c, $in) : '') . (is_null($k) ? '' : "'$k'"
                    . str_repeat($c, $lenkeys) . "=> " . $eval['val'] . "["
                    . str_repeat(" ", $lenkey - 4) . "// "
                    . $eval['desc']) . (empty($v) ? '' : PHP_EOL);
                
            foreach ($v as $sk => $vl) {
                $r .= $pretty($indents, $varlentitle, $vl, $c, $in + 4, $sk) . PHP_EOL;
            }
            $r .= (empty($v) ? '],' : ($in != 0 ? str_repeat($c, $in / 2) : '') .
                (is_null($v) ? '' : str_repeat($c, $in / 2) . "],"));
        }

    } else {

        if (mb_strpos($k, chr(0)) !== false) {
            $k = str_replace(chr(0), "", $k);
        }

        $lenkey = $indents['key'] - mb_strlen("'$k'") - $in;
        if ($lenkey < 0) {
            $lenkey = 0;
        }
        $eval   = $this->evaluateVariable($v);
        $lenval = $indents['val'] - (mb_strlen("'" . $eval['val'] . "'"));

        if ($lenval < 0) {
            $lenval = 0;
        }
        $r .= ($in != -1 ? str_repeat($c, $in) : '') . (is_null($k) ? '' : "'$k'"
                . str_repeat($c, $lenkey) . '=> ') . $eval['val']
            . str_repeat(" ", $lenval) . '// ' . $eval['desc'];
    }

    return str_replace("\0", "", $r);
};

// DO WE EVEN RECACH HERE?
// There is a return in the recursive function 
// and I'm not sure if we even reach down low.

$varlentitle = mb_strlen('$' . $varname);

if (in_array(gettype($data), ['object', 'array'])) {
    $string = '$' . $varname . str_repeat(" ", (($indents['key'] - $varlentitle) >= 0 ? $indents['key'] - $varlentitle : 1)) . '= ['
        . str_repeat(" ", $indents['val'] - 2) . '// main array node.'
        . rtrim($pretty($indents, $varlentitle, $data), ',') . ';';
} else {
    $eval   = $this->evaluateVariable($data);
    $string = '$' . $varname . str_repeat(" ", $indents['key']) . '=' . $eval['val'] . ';'
        . str_repeat(" ", $indents['val'] - 1) . '// ' . $eval['desc'];
}

return $string;

}

Function evaluateVariable()

Well improvements can be here as well

if (is_object($var)) {
    // Is there any specific reason for it?
    // I am not able to figure out!
    ob_start();
    $string = explode('{', ob_get_clean());
    return ['val' => '(object) ', 'desc' => rtrim(reset($string)) . '.'];
}

// Im not sure why we need to do this?
// What are we storing in buffer and 
// reusing? And we are returning so what
// will be the purpose of doing this?
ob_start();
$string = ob_get_clean();
if (mb_strpos($string, 'resource') !== false) {
    return ['val' => 'resource', 'desc' => rtrim($string) . '.'];
} elseif (mb_strpos($string, 'of type ') !== false) {
    return ['val' => 'resource', 'desc' => rtrim($string) . '.'];
}
unset($string);
   

Conclusion

Well those were some things I could see but that is just my perception. There is a lot of room for improvement and I can see there are good signs that your are trying to do that, which is a promising sign :).

The first thing I would recommend I go through Clean Code, SOLID Principles, and Write Unit Test. PSR has already been mentioned so it would be good to follow them as well.

As I read from one of your replies that is in your future plan, I would say amend that plan immediately as writing test will help you become a good software engineer and a better programmer! And writing test is not that hard, at all!

Here is a brief article on Unit Test : https://codeanit.medium.com/developers-guide-write-good-test-5e3e3cdec78e

To further improve the quality of your code you can use Static Analysis Tools, an example: https://github.com/squizlabs/PHP_CodeSniffer

And one last thing I would like to add more is practice data structure and algorithm usage in PHP. Use tools like HackerRank, Codility, etc.. to practice problem solving and code challenges which will definitely make you better. As from the hard review of the code, I definitely can also see a well structural implemented system.

I wish you all the very best!

\$\endgroup\$
9
  • \$\begingroup\$ Hello friend, thanks for your feedback; This question was already verified and resolved at the time. but currently there is another open reward candidate publication based on the latest update, which includes documentation on each objective of each function. \$\endgroup\$ – Francisco Núñez-Todo Poderoso Mar 1 at 18:25
  • \$\begingroup\$ New POST: codereview.stackexchange.com/questions/256491/… \$\endgroup\$ – Francisco Núñez-Todo Poderoso Mar 1 at 18:25
  • \$\begingroup\$ Github repository with all the documentation to test: github.com/arcanisgk/BOH-Basic-Output-Handler \$\endgroup\$ – Francisco Núñez-Todo Poderoso Mar 1 at 18:25
  • \$\begingroup\$ in the same way I have taken note of everything indicated for version v1.0.6, thank you very much \$\endgroup\$ – Francisco Núñez-Todo Poderoso Mar 1 at 18:26
  • 1
    \$\begingroup\$ Linux/Windows/Mac, It does not matter which platform you are in @FranciscoNúñez! If you seriously interested to improve yourself as a developer you will find a way to write test anyways. And it's easy to run from CLI - php file.php. Also I recommend you to use a debugger. I know it sounds a lot at first but anyone who is seriously looking to make a good career, she/he will have to learn to do the hard stuffs. Anyways, again I wish all the very best! \$\endgroup\$ – Anit Shrestha Manandhar Mar 3 at 4:12

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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