1
\$\begingroup\$

First of all I must thank for the help given in the previous post:

Class showing a format similar to var_dump RC5 Version

I have it deployed in a early versio v1.0.5: https://github.com/arcanisgk/BOH-Basic-Output-Handler

As they should imagine; Thanks to your help, the main code has had serious changes; both of logic and structure and I have also implemented a series of new ideas; including CLI support.

Even so I have some doubts since I am not an expert in php.

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

  1. I have implemented a number of methods to control and display specific layouts in the data output, mainly the color palette is manipulated, both for the Web and for the CLI. I have listened to different proposals which I have mixed and implemented up to the present point:

related properties: TOKENSLIST , THEMESLIST,$colorcli.

related method: __construct, resetHighlight, getTheme, setHighlightTheme, setHighlightThemeCli, colorRGBforCLI, highlightCode, highlightCodeCli, adjusterSpaceLine and applyCss

fixed before someone replied to this post

2. The getIndent method uses the parsing of the variable to determine the indentation of the output using the array_walk_recursive function. I don't have a lot of knowledge about handling arrays therefore I used this method because I found it simple. I don't know if it is the best to fulfill the purpose of the getIndent method. but I have already verified that in the case of magic methods stored in a class variable; the correct indentation number is not obtained or even if it is a class object that has properties, the name of this is not displayed as it should be:

class FooBar
{
    public string $pub_string = 'hello world!';
    private array $priv_array = ['a' => 1, 'b' => 2];
    const CONST_OBJECT = ['a' => 1, 'b' => 2];

    function foofunction()
    {
        return "Hello World!";
    }
}

$varclass = new FooBar;

$output = new Output\OutputHandler();
$output->getTheme('monokai');
$output->output([$varclass]);

if I take a look at it from var_dump it looks like this:

object(FooBar)#3 (2) {
  ["pub_string"]=>
  string(12) "hello world!"
  ["priv_array":"FooBar":private]=>
  array(2) {
    ["a"]=>
    int(1)
    ["b"]=>
    int(2)
  }
}

example output, I would like the name that tried to capture to be 'FooBar::priv_array' but it comes out like this:

  1. The "analyzeVariable" method uses the abstraction of the "pretty" function. It was the only example I found where it allowed me to have output similar to 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 I want to give it. I also have an analysis group to space out using ternary analysis. I would like someone with more experience to review it, looking for possible errors or improvements, to the ternary analysis that is executed. With the minimal tests that I have carried out I have not obtained any error.

  2. The evaluaVariable 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()
    
  3. Another thing that I also think can negatively impact the performance of the library is the analysis performed by the evaluateVariable method since it is a set of if checks, improvements have already been made but I think it can be improved even more, I hear opinions about.

  4. Another important point for me is to evaluate if, in general, the Script / library complies with the active standards of PSR 1 and 12. I use phpStorm IDE but it does not detect an error of this type, several have already been corrected if you see any favor to tell me.

  5. for version v1.1.0 I hope I have implemented unit tests; I have no knowledge on this topic currently.

  6. for version v1.1.0 I hope to have implemented the skipping envelope for highlight_file files.

for point 7 and 8 I am also waiting to hear any suggestions.

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.

My code

<?php

/**
 * BOHBasicOutputHandler - Data output manager in PHP development environments.
 * PHP Version 7.4.
 *
 * @see https://github.com/arcanisgk/BOH-Basic-Output-Handler
 *
 * @author    Walter Nuñez (arcanisgk/original founder) <[email protected]>
 * @copyright 2020 - 2021 Walter Nuñez.
 * @license   http://www.gnu.org/copyleft/lesser.html GNU Lesser General Public License
 * @note      This program is distributed in the hope that it will be useful
 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
 * or FITNESS FOR A PARTICULAR PURPOSE.
 */

namespace IcarosNet\BOHBasicOutputHandler;

/**
 * BOHBasicOutputHandler - Data output manager in PHP development environments.
 *
 * @author    Walter Nuñez (arcanisgk/original founder) <[email protected]>
 */

/**
 * Validation of php version.
 * strictly equal to or greater than 7.4
 * a minor version will kill any script.
 *
 */

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

/**
 * Validation of the environment of use.
 * support for web and cli environments
 *
 */

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

class OutputHandler
{

    /**
     * theme selected by implementer.
     * Options: null (default), __construct update to 'default' or theme selected.
     * 'default','monokai','natural-flow','mauro-dark','x-space'
     *
     * @var string
     */
    public string $themeused;

    /**
     * capture the environment for usage in the class.
     * Options: empty (default), __construct update to 'ENVIRONMENT_OUTPUT_HANDLER'
     * constant or implementor defined environment.
     * 'cli','web'
     *
     * @var string
     */
    public string $defenv = '';

    /**
     * List of TOKENS and Respective flags for Cli Themes.
     *
     * @var array
     */
    const TOKENSLIST = [
        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              => "",
    ];

    /**
     * List of CURENCY and Respective flags for Cli Themes.
     *
     * @var array
     */
    const CURRENCIESLIST = [
        '¤', '$', '¢', '£', '¥', '₣', '₤', '₧', '€', '₹', '₩', '₴', '₯', '₮',
        '₰', '₲', '₱', '₳', '₵', '₭', '₪', '₫', '₠', '₡', '₢', '₥', '₦', '₨',
        '₶', '₷', '₸', '₺', '₻', '₼', '₽', '₾', '₿'
    ];

    /**
     * List of default themes colors.
     *
     * @var array
     */
    const THEMESLIST = [
        'x-space'      => ['043,128,041', '099,099,099', '128,128,128', '072,094,187', '221,079,079', '000,000,000'],
        'mauro-dark'   => ['187,134,252', '250,250,250', '003,218,197', '255,204,255', '207,102,121', '018,018,018'],
        'natural-flow' => ['145,155,152', '030,156,107', '003,218,197', '006,156,004', '139,156,051', '004,041,003'],
        'monokai'      => ['117,113,094', '255,255,255', '102,217,239', '249,038,114', '230,219,116', "039,040,034"],
        'default'      => ['255,095,000', '000,000,255', '000,000,000', '000,175,000', '255,000,000', '255,255,255'],
    ];

    /**
     * definition of colors for implementation in CLI.
     *
     * @var array
     */
    public array $colorcli = [
        "comment"    => '',
        "constant"   => '',
        "function"   => '',
        "keyword"    => '',
        "magic"      => '',
        "string"     => '',
        "tag"        => '',
        "variable"   => '',
        "html"       => '',
        ""           => "%s",
        "background" => '',
    ];

    /**
     * Constructor.
     *
     * @param  string  $theme
     */
    public function __construct($theme = 'default')
    {
        $this->getTheme($theme);
        $this->defenv = ENVIRONMENT_OUTPUT_HANDLER;
    }

    /**
     * Destructor.
     */
    public function __destruct()
    {
        $this->resetHighlight();
    }

    /**
     * Call for reset of Theme colors in Web.
     */
    public function resetHighlight(): void
    {
        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");
    }

    /**
     * Call theme() for theme select by implementer.
     *
     * @param  string  $theme
     * Options: 'default' (default),'monokai','natural-flow','mauro-dark','x-space'
     */
    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';
        }
        $this->setHighlightTheme($color);
        $this->setHighlightThemeCli($color);
    }

    /**
     * Sets color of theme selected for web design.
     *
     * @param  array  $color
     */
    private function setHighlightTheme(array $color): void
    {
        ini_set("highlight.comment", 'rgb(' . $color[0] . '); background-color: rgb(' . $color[5] . ');');
        ini_set("highlight.default", 'rgb(' . $color[1] . '); background-color: rgb(' . $color[5] . ');');
        ini_set("highlight.html", 'rgb(' . $color[2] . '); background-color: rgb(' . $color[5] . ');');
        ini_set("highlight.keyword", 'rgb(' . $color[3] . "); font-weight: bold; background-color: rgb(" . $color[5] . ');');
        ini_set("highlight.string", 'rgb(' . $color[4] . '); background-color: rgb(' . $color[5] . ');');
    }

    /**
     * Sets color of theme selected for cli design.
     *
     * @param  array  $color
     */
    private function setHighlightThemeCli(array $color): void
    {
        $this->colorcli['comment']    = "\033[38;2;" . $this->colorRGBforCLI($color[0]) . "m%s\033[0m";
        $this->colorcli['constant']   = "\033[38;2;" . $this->colorRGBforCLI($color[4]) . "m%s\033[0m";
        $this->colorcli['function']   = "\033[38;2;" . $this->colorRGBforCLI($color[1]) . "m%s\033[0m";
        $this->colorcli['keyword']    = "\033[38;2;" . $this->colorRGBforCLI($color[3]) . "m%s\033[0m";
        $this->colorcli['magic']      = "\033[38;2;" . $this->colorRGBforCLI($color[1]) . "m%s\033[0m";
        $this->colorcli['string']     = "\033[38;2;" . $this->colorRGBforCLI($color[4]) . "m%s\033[0m";
        $this->colorcli['tag']        = "\033[38;2;" . $this->colorRGBforCLI($color[1]) . "m%s\033[0m";
        $this->colorcli['variable']   = "\033[38;2;" . $this->colorRGBforCLI($color[3]) . "m%s\033[0m";
        $this->colorcli['html']       = "\033[38;2;" . $this->colorRGBforCLI($color[2]) . "m%s\033[0m";
        $this->colorcli['background'] = "\033[48;2;" . $this->colorRGBforCLI($color[5]) . "m";
    }

    /**
     * Convert RGB color String from web standard to ANSI color .
     *
     * @param  string  $color
     *
     * @return string
     */
    private function colorRGBforCLI(string $color): string
    {
        return str_replace(',', ';', $color);
    }

    /**
     * Convert normal string output of variable to
     * String highlight like php code for web output
     *
     * @param  string  $string
     *
     * @return string
     */
    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);
    }

    /**
     * Convert normal string output of variable to
     * String highlight like php code for cli output
     *
     * @param  string  $string
     *
     * @return string
     */
    protected function highlightCodeCli(string $string): string
    {
        $bg     = $this->colorcli['background'];
        $string = '<?php' . PHP_EOL . $string . PHP_EOL . '?>';
        $string = $this->adjusterSpaceLine($string);
        $colors = $this->colorcli;
        $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(self::TOKENSLIST[$t])) {
                    $output .= $bg . sprintf($colors[self::TOKENSLIST[$t]], $str) . "\033[0m";
                } else {
                    $output .= $bg . sprintf("<%s '%s'>", token_name($t), $str) . "\033[0m";
                }
            }
        }
        return $output;
    }

    /**
     * space adjuster at the end of the line for full background coverage in cli
     *
     * @param  string  $string
     *
     * @return string
     */
    private function adjusterSpaceLine(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;
    }

    /**
     * CSS applicator for web design.
     *
     * @param  string  $string
     *
     * @return string
     */
    private function applyCss(string $string): string
    {
        $bg    = 'rgb(' . self::THEMESLIST[$this->themeused][5] . ')';
        $class = mt_rand();
        return '<style>
                    .outputhandler-' . $class . '{
                        background-color: ' . $bg . '; padding: 8px;
                        border-radius: 8px;
                        margin: 5px;
                    }
                    .outputhandler-' . $class . ' > code {
                    padding: unset;
                    }
                </style>
                <div class="outputhandler-' . $class . '">' . $string . '</div>';
    }

    /**
     * environment checker; if the implementer is wrong;
     * the library will abort any execution immediately
     * and display an error message stating that it has happened.
     *
     * @param  null|string  $env
     *
     * @return string
     */
    private function checkEnv($env): string
    {
        $iscli = IsCommandLineInterface();
        $env   = $env ?? $this->defenv;
        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;
    }

    /**
     * Check and Execute the request to show the formatted data.
     *
     * @param  mixed  $var
     * @param  null|string  $env
     * @param  bool  $retrieve
     *
     * @return void|string
     */
    public function output($var, $env = null, $retrieve = false)
    {
        $env = $this->checkEnv($env);
        if ($env == 'web') {
            $string = $this->outputWb($var, $retrieve);
        } elseif ($env == 'cli') {
            $string = $this->outputCli($var, $retrieve);
        } else {
            $string = $this->outputWb($var, $retrieve);
        }
        if ($retrieve) {
            return $string;
        }
    }

    /**
     * Check and Execute the request to show the formatted data for web environment.
     *
     * @param  mixed  $var
     * @param  bool  $retrieve
     *
     * @return void|string
     */
    public function outputWb($var, $retrieve = false)
    {
        $indents = $this->getIndent($var);
        $string  = $this->analyzeVariable($var, $indents);
        $string  = $this->highlightCode($string);
        $string  = $this->applyCss($string);
        $this->resetHighlight();
        return ($retrieve ? $string : $this->outView($string));
    }

    /**
     * Check and Execute the request to show the formatted data for cli environment.
     *
     * @param  mixed  $var
     * @param  bool  $retrieve
     *
     * @return void|string
     */
    public function outputCli($var, $retrieve = false)
    {
        $indents = $this->getIndent($var);
        $string  = $this->analyzeVariable($var, $indents);
        $string  = $this->highlightCodeCli($string);
        $this->resetHighlight();
        return ($retrieve ? $string : $this->outView($string));
    }

    /**
     * Evaluates the indentation that the values and
     * comments should have in the construction of the output
     *
     * @param  mixed  $var
     *
     * @return array
     */
    private function getIndent($var): array
    {
        $data    = $var;
        $indents = ['key' => 0, 'val' => 0];
        if (is_array($data) || is_object($data)) {
            $data = (array) $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) {
                if (mb_strpos($key, chr(0)) !== false) {
                    $key = str_replace(chr(0), "'::'", $key);
                    $key = substr($key, 4);
                }
                $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;
    }

    /**
     * Calculates how many nodes deep the passed variable has if it is an array or object.
     * note: it does not calculate the number of total nodes.
     *
     * @param  array  $array
     *
     * @return int
     */
    private function calcDeepArray(array $array): int
    {
        $max_depth = 0;
        foreach ($array as $key => $value) {
            if (is_array($value) || is_object($value)) {
                $depth = $this->calcDeepArray((array) $value) + 1;
                if ($depth > $max_depth) {
                    $max_depth = $depth;
                }
            }
        }
        return $max_depth;
    }

    /**
     * This should parse each variable passed and build the output string,
     * similar to var_dump or var_export.
     *
     * @param  mixed  $var
     * @param  array  $indents
     *
     * @return string
     */
    protected function analyzeVariable($var, array $indents): string
    {
        $varname     = 'variable';
        $data        = $var;
        $pretty      = function ($indents, $varlentitle, $v = '', $c = " ", $in = 0, $k = null) use (&$pretty) {
            $r = '';
            if (in_array(gettype($v), ['object', 'array'])) {
                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);
        };
        $varlentitle = mb_strlen('$' . $varname);
        if (in_array(gettype($var), ['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;
    }

    /**
     * This should analyze each variable passed indicate the value and description of it.
     * note: the description is a rich text.
     *
     * @param  mixed  $var
     *
     * @return array
     */
    protected function evaluateVariable($var): array
    {
        if (null === $var || 'null' === $var || 'NULL' === $var) {
            return is_string($var) ? ['val' => "'null'", 'desc' => 'null value string.'] :
                ['val' => 'null', 'desc' => 'null value.'];
        }

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

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

        }

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

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

        if ((float) $var == $var && is_numeric($var)) {
            return is_string($var) ? ['val' => "'" . $var . "'", 'desc' => '(' . mb_strlen($var) . ') float value string.'] :
                ['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->splitStrToUnicode($var);
            $currencycheck = [];
            foreach ($arr as $char) {
                if (in_array($char, self::CURRENCIESLIST, 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' => 'unknown', 'desc' => 'unknown'];
    }

    /**
     * This should validate Date String.
     *
     * @param  string  $date
     *
     * @return bool
     */
    private function validateDate(string $date): bool
    {
        return (strtotime($date) !== false);
    }

    /**
     * This should cut the strings in unicode format.
     *
     * @param  string  $str
     * @param  int  $length  default 1
     *
     * @return array
     */
    private function splitStrToUnicode(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;
    }

    /**
     * This should send the text on screen.
     *
     * @param  string  $string
     */
    private function outView(string $string): void
    {
        echo $string;
    }
}

/**
 * check if runtime environment is CLI
 *
 * @return bool
 */
function IsCommandLineInterface(): bool
{
    return (php_sapi_name() === 'cli');
}

Example Usage

<?php

/**
 * This example shows how the BOHBasicOutputHandler class and its methods are declared.
 */

//Import the PHPMailer class into the global namespace
use \IcarosNet\BOHBasicOutputHandler as Output;

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

/**
 * FooBar is an example class.
 */
class FooBar
{
    public string $pub_string = 'hello world!';
    protected int $pro_int = 10;
    private array $priv_array = ['a' => 1, 'b' => 2];
    const CONST_OBJECT = ['a' => 1, 'b' => 2];

    public function foofunction()
    {
        return "Hello World!";
    }

    protected function foofunction2()
    {
        return "Hello World!";
    }
}

/**
 * $varclass is a variable storage of instance class FooBar.
 */
$varclass = new FooBar;

/**
 * $examplesingle is a short variable to use as an example.
 */
$examplesingle = 'Hello World';

/**
 * $exampleshortarray is a short variable to use as an example.
 */
$exampleshortarray = ['a' => 1, 'b' => 2];

/**
 * $examplearray is a large array variable to use as an example.
 */
$examplearray = [
    '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'        => [
        'boolean_true'       => true,
        'boolean_false'      => false,
        'boolean_true_text'  => 'true',
        'boolean_false_text' => 'false',
        'object'             => (object) [
            'key_index_most' => 'Hello Wolrd',
            'joder'          => [
                'prueba' => 'prueba',
            ]
        ],
        'nested'             => [
            'other_obj' => (object) [
                'apple',
                'banana',
                'coconut',
            ],
        ],
    ],
    'objects_list' => [
        'object_empty' => (object) [],
        'class'        => $varclass,
        'resource'     => curl_init(),
    ],
];

//Instance Class BOHBasicOutputHandler
$output = new Output\OutputHandler();

//Theme Selection
$output->getTheme('monokai');

//example 1:
$output->output($examplearray);

$output->getTheme('natural-flow');

//example 2:
$output->output($examplearray);

$output->getTheme('x-space');

//example 3:
$output->output($exampleshortarray);

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
  • \$\begingroup\$ You should take another look at the way you have implemented "themes" - I feel they would be much better defined outside your class and tied together with a Theme interface... \$\endgroup\$
    – DaveRandom
    Commented Mar 2, 2021 at 0:13

1 Answer 1

1
+50
\$\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 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\$
3
  • \$\begingroup\$ Access specifiers They are used Private, Protected, and Public, which is great to see, but I think a lot of thought has gone into doing it. You can be more specific? \$\endgroup\$
    – user152284
    Commented Mar 1, 2021 at 18:46
  • \$\begingroup\$ Class: you have some example of how to implement abstract classes \$\endgroup\$
    – user152284
    Commented Mar 1, 2021 at 18:50
  • \$\begingroup\$ @FranciscoNúñez I am sure you will definitely good examples of OOP in PHP. You can put the examples you have found and put that implementation up for review. But for now I would leave it up to you as it took some time to review the code and I would love see what you can come up from this. For reference: github.com/php-cheatsheet, you might find something there. All the very best! \$\endgroup\$ Commented Mar 2, 2021 at 4:33

Your Answer

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