Selaa lähdekoodia

nitial release of PHP VarDumper

michelphp 1 päivä sitten
sitoutus
7e820fea65

+ 3 - 0
.gitignore

@@ -0,0 +1,3 @@
+/vendor
+/.idea
+composer.lock

+ 21 - 0
LICENSE

@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2025 MICHEL
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.

+ 180 - 0
README.md

@@ -0,0 +1,180 @@
+# Michel PHP VarDumper
+
+A lightweight and versatile PHP library for debugging and inspecting variables with customizable output formats.
+
+## Installation
+
+You can install this library via [Composer](https://getcomposer.org/). Ensure your project meets the minimum PHP version requirement of 7.4.
+
+```bash
+composer require michel/vardumper
+```
+
+## Requirements
+
+- PHP version 7.4 or higher
+
+## Usage
+
+The library provides two main functions for debugging: `dump()` and `dd()`. These functions allow you to inspect variables in a readable format, with support for both CLI and HTML output.
+
+### `dump()` Function
+
+The `dump()` function outputs the variable(s) passed to it without terminating the script. This is useful for inspecting variables during the execution of your code.
+
+```php
+<?php
+
+require 'vendor/autoload.php';
+
+$data = [
+    'name' => 'John Doe',
+    'age' => 30,
+    'email' => 'john.doe@example.com'
+];
+
+dump($data);
+```
+
+### `dd()` Function
+
+The `dd()` function (short for "dump and die") outputs the variable(s) passed to it and then terminates the script. This is useful for debugging and inspecting variables at a specific point in your code.
+
+```php
+<?php
+
+require 'vendor/autoload.php';
+
+$data = [
+    'name' => 'John Doe',
+    'age' => 30,
+    'email' => 'john.doe@example.com'
+];
+
+dd($data);
+```
+
+### Output Formats
+
+The library automatically detects the environment (CLI or web) and formats the output accordingly. You can also manually specify the output format if needed.
+
+#### CLI Output
+
+When running in a CLI environment, the output is formatted for readability in the terminal.
+
+```php
+<?php
+
+require 'vendor/autoload.php';
+
+$data = [
+    'name' => 'John Doe',
+    'age' => 30,
+    'email' => 'john.doe@example.com'
+];
+
+dump($data);
+```
+
+**CLI Output Example:**
+
+```
+array(3) [
+  [name] => (string) "John Doe"
+  [age] => (int) 30
+  [email] => (string) "john.doe@example.com"
+]
+```
+
+#### HTML Output
+
+When running in a web environment, the output is formatted with HTML for better readability in the browser.
+
+```php
+<?php
+
+require 'vendor/autoload.php';
+
+$data = [
+    'name' => 'John Doe',
+    'age' => 30,
+    'email' => 'john.doe@example.com'
+];
+
+dump($data);
+```
+
+**HTML Output Example:**
+
+```html
+<div id="var_dump_7426e40db5b46be3c13051ff19d2eff0" class="__beautify-var-dumper">
+    <span class="type caret caret-down" data-target="#target_0fa6bec56ef91af4269ff3ac1ec702e4">array</span> 
+    <small><i>(Size: 3)</i></small> (
+    <br>
+    <div class="nested active" id="target_0fa6bec56ef91af4269ff3ac1ec702e4">
+        <span class="key">name</span> =&gt; 
+        <span class="string">
+            <span class="type">string</span> 'John Doe'
+        </span> 
+        <small><i>(Lenght: 8)</i></small>
+        <br>
+        <span class="key">age</span> =&gt; 
+        <span class="number">
+            <span class="type">int</span> 30
+        </span>
+        <br>
+        <span class="key">email</span> =&gt; 
+        <span class="string">
+            <span class="type">string</span> 'john.doe@example.com'
+        </span> 
+        <small><i>(Lenght: 20)</i></small>
+        <br>)
+    </div>
+</div>
+```
+
+### Custom Output
+
+You can also customize the output by passing a custom `OutputInterface` implementation to the `VarDumper` constructor.
+
+```php
+<?php
+
+require 'vendor/autoload.php';
+
+use Michel\Debug\VarDumper;
+use Michel\Debug\Output\OutputInterface;
+
+class CustomOutput implements OutputInterface
+{
+    public function print($data): void
+    {
+        // Custom output logic here
+        echo "Custom Output: " . print_r($data, true);
+    }
+}
+
+$data = [
+    'name' => 'John Doe',
+    'age' => 30,
+    'email' => 'john.doe@example.com'
+];
+
+$varDumper = new VarDumper(new CustomOutput());
+$varDumper->dump($data);
+```
+
+## Screenshots
+
+
+### CLI Mode
+
+![CLI Mode Screenshot](screenshot/cli_output.png)
+
+### HTML Mode
+
+![HTML Mode Screenshot](screenshot/html_output.png)
+
+## License
+
+This library is open-source software licensed under the MIT license. See the [LICENSE](LICENSE) file for more information.

+ 27 - 0
composer.json

@@ -0,0 +1,27 @@
+{
+  "name": "michel/vardumper",
+  "description": "A lightweight and versatile PHP library for debugging and inspecting variables with customizable output formats.",
+  "type": "library",
+  "license": "MIT",
+  "authors": [
+    {
+      "name": "F. Michel"
+    }
+  ],
+  "autoload": {
+    "psr-4": {
+      "Michel\\Debug\\": "src",
+      "Test\\Michel\\Debug\\": "tests"
+    },
+    "files": [
+      "functions/helpers.php"
+    ]
+  },
+  "require": {
+    "php": ">=7.4",
+    "ext-json": "*"
+  },
+  "require-dev": {
+    "michel/unitester": "^1.0.0"
+  }
+}

+ 53 - 0
exemples/clioutput.php

@@ -0,0 +1,53 @@
+<?php
+
+require dirname(__DIR__) . '/vendor/autoload.php';
+
+use Michel\Debug\VarDumper;
+
+$data = [
+    'name' => 'John Doe',
+    'email' => 'john.doe@example.com',
+    'active' => true,
+    'roles' => ['admin', 'user'],
+    'file' => new SplFileInfo(__FILE__)
+];
+
+dump($data);
+
+$data = new stdClass();
+$data->name = 'John Doe';
+$data->email = 'john.doe@example.com';
+$data->active = true;
+dump($data);
+
+dump(true);
+dump(false);
+
+
+$data = "Hello\nWorld\t😊";
+dump($data);
+
+$data = [
+    'string' => 'Hello world',
+    'int' => 42,
+    'float' => 3.14,
+    'boolTrue' => true,
+    'boolFalse' => false,
+    'nullValue' => null,
+    'arraySimple' => [1, 2, 3],
+    'arrayNested' => [
+        'level1' => [
+            'level2' => [
+                'level3a' => 'deep',
+                'level3b' => [4, 5, 6]
+            ],
+            'level2b' => 'mid'
+        ],
+        'anotherKey' => 'value'
+    ],
+    'objectSimple' => (object)['foo' => 'bar', 'baz' => 123],
+];
+$func = function ()  use ($data) {
+    dd_bt($data);
+};
+$func();

+ 65 - 0
exemples/htmloutput.php

@@ -0,0 +1,65 @@
+<?php
+
+require dirname(__DIR__) . '/vendor/autoload.php';
+
+use Michel\Debug\BacktraceDumper;
+use Michel\Debug\Output\VarDumperOutput\HtmlOutput;
+use Michel\Debug\VarDumper;
+
+function _ddbt($data)
+{
+    $dumper = new VarDumper(new HtmlOutput());
+    $backtraceDumper = new BacktraceDumper(new \Michel\Debug\Output\BacktraceOutput\HtmlOutput());
+    $backtraceDumper->dump();
+    $dumper->dump($data);
+    die(1);
+}
+
+$dumper = new VarDumper(new HtmlOutput());
+$data = [
+    'name' => 'John Doe',
+    'email' => 'john.doe@example.com',
+    'active' => true,
+    'roles' => ['admin', 'user'],
+    'file' => new SplFileInfo(__FILE__)
+];
+$dumper->dump($data);
+
+$data = new stdClass();
+$data->name = 'John Doe';
+$data->email = 'john.doe@example.com';
+$data->active = true;
+$dumper->dump($data);
+
+
+$dumper->dump(true);
+$dumper->dump(false);
+
+
+$data = "Hello\nWorld\t😊";
+$dumper->dump($data);
+
+$data = [
+    'string' => 'Hello world',
+    'int' => 42,
+    'float' => 3.14,
+    'boolTrue' => true,
+    'boolFalse' => false,
+    'nullValue' => null,
+    'arraySimple' => [1, 2, 3],
+    'arrayNested' => [
+        'level1' => [
+            'level2' => [
+                'level3a' => 'deep',
+                'level3b' => [4, 5, 6]
+            ],
+            'level2b' => 'mid'
+        ],
+        'anotherKey' => 'value'
+    ],
+    'objectSimple' => (object)['foo' => 'bar', 'baz' => 123],
+];
+$func = function () use ($data) {
+    _ddbt($data);
+};
+$func();

+ 73 - 0
functions/helpers.php

@@ -0,0 +1,73 @@
+<?php
+
+if (!function_exists('btrace')) {
+
+
+    /**
+     *  Dump backtrace
+     * @param int $backtraceLimit
+     * @return void
+     */
+    function btrace(int $backtraceLimit = 5)
+    {
+        $backtraceDumper = new \Michel\Debug\BacktraceDumper();
+        $backtraceDumper->dump($backtraceLimit);
+    }
+}
+
+if (!function_exists('dd_bt')) {
+
+    /**
+     * Dump with debug trace: Dumps data and exits the script.
+     *
+     * @param mixed $data The data to dump.
+     */
+    function dd_bt($data, int $backtraceLimit = 5)
+    {
+        btrace($backtraceLimit);
+        dd($data);
+    }
+}
+
+if (!function_exists('dd')) {
+
+    /**
+     * Dump and die: Dumps data and exits the script.
+     *
+     * @param mixed ...$data The data to dump.
+     */
+    function dd(...$data)
+    {
+        dump(...$data);
+        exit(1);
+    }
+}
+
+if (!function_exists('dump')) {
+
+    /**
+     * Dump data to the output.
+     *
+     * @param mixed ...$data The data to dump.
+     */
+    function dump(...$data)
+    {
+        $varDumper = new \Michel\Debug\VarDumper();
+        $varDumper->dump(...$data);
+    }
+}
+
+if (!function_exists('console_log')) {
+
+    /**
+     * Log data to the javascript console.
+     *
+     * @param mixed ...$data The data to log.
+     */
+    function console_log(...$data)
+    {
+        $varDumper = new \Michel\Debug\VarDumper(new \Michel\Debug\Output\VarDumperOutput\ConsoleLogOutput());
+        $varDumper->dump(...$data);
+    }
+
+}

+ 36 - 0
resources/css/backtrace.css

@@ -0,0 +1,36 @@
+.__beautify-backtrace-container {
+    all : unset;
+    font-family: monospace;
+    display: block;
+    max-height: 400px;
+    overflow-y: auto;
+    margin-bottom: 5px;
+    border: 1px solid #44475a;
+    border-radius: 6px;
+    background-color: #1e1f29;
+}
+
+.__beautify-backtrace-title {
+    all : unset;
+    font-family: monospace;
+    display: block;
+    margin-bottom: 5px;
+    color: #29a2e0;
+    padding: 5px;
+    font-size: 12px;
+}
+.__beautify-backtrace-dumper {
+    all : unset;
+    display: block;
+    box-sizing: border-box;
+    background-color: #24292e;
+    color: #f8f8f2;
+    padding: 5px;
+    border-radius: 5px;
+    font-family: monospace;
+    font-size: 11px;
+    white-space: pre-wrap;
+    overflow: auto;
+    width: 100%;
+    margin-bottom: 2px;
+}

+ 68 - 0
resources/css/dump.css

@@ -0,0 +1,68 @@
+#uniqId.__beautify-var-dumper {
+    box-sizing: border-box;
+    background-color: #24292e;
+    color: #f8f8f2;
+    padding: 10px;
+    border-radius: 5px;
+    font-family: monospace;
+    font-size: 13px;
+    white-space: pre-wrap;
+    overflow: auto;
+    width: 100%;
+    margin-bottom: 5px;
+}
+#uniqId.__beautify-var-dumper .key {
+    color: #e89d69;
+}
+#uniqId.__beautify-var-dumper .string {
+    color: #79b8ff;
+}
+#uniqId.__beautify-var-dumper .float, #uniqId.__beautify-var-dumper .int {
+    color: #ae81ff;
+}
+#uniqId.__beautify-var-dumper .boolean {
+    color: #f92672;
+}
+#uniqId.__beautify-var-dumper .null {
+    color: #f92672;
+}
+#uniqId.__beautify-var-dumper .type {
+    color: #e1f7fc;
+    font-style: italic;
+    font-size: 12px;
+}
+
+#uniqId.__beautify-var-dumper .caret {
+    cursor: pointer;
+    -webkit-user-select: none; /* Safari 3.1+ */
+    -moz-user-select: none; /* Firefox 2+ */
+    -ms-user-select: none; /* IE 10+ */
+    user-select: none;
+    font-style: normal !important;
+}
+
+
+/* Create the caret/arrow with a unicode, and style it */
+#uniqId.__beautify-var-dumper .caret::before {
+    content: "\25B6";
+    color: #7ce3fa;
+    display: inline-block;
+    margin-right: 6px;
+}
+
+/* Rotate the caret/arrow icon when clicked on (using JavaScript) */
+#uniqId.__beautify-var-dumper .caret-down::before {
+    -ms-transform: rotate(90deg); /* IE 9 */
+    -webkit-transform: rotate(90deg); /* Safari */'
+    transform: rotate(90deg);
+}
+
+/* Hide the nested list */
+#uniqId.__beautify-var-dumper .nested {
+    display: none;
+}
+
+/* Show the nested list when the user clicks on the caret/arrow (with JavaScript) */
+#uniqId.__beautify-var-dumper .active {
+    display: block;
+}

+ 3 - 0
resources/js/console.log.js

@@ -0,0 +1,3 @@
+console.group('[VarDumper]');
+console.debug([value_to_debug]);
+console.groupEnd();

+ 15 - 0
resources/js/debug.js

@@ -0,0 +1,15 @@
+
+document.addEventListener("DOMContentLoaded", (event) => {
+    let toggler = document.querySelectorAll("#uniqId.__beautify-var-dumper .caret");
+    let i;
+    for (i = 0; i < toggler.length; i++) {
+        toggler[i].addEventListener("click", function(element) {
+            const target = this.getAttribute("data-target");
+            const targetElem = document.querySelector(target);
+            if (targetElem) {
+                targetElem.classList.toggle("active");
+                this.classList.toggle("caret-down");
+            }
+        });
+    }
+});

BIN
screenshot/cli_output.png


BIN
screenshot/html_output.png


+ 39 - 0
src/BacktraceDumper.php

@@ -0,0 +1,39 @@
+<?php
+
+namespace Michel\Debug;
+
+use Michel\Debug\Output\BacktraceOutput\CliOutput;
+use Michel\Debug\Output\BacktraceOutput\HtmlOutput;
+use Michel\Debug\Output\OutputInterface;
+
+final class BacktraceDumper
+{
+
+    private OutputInterface $output;
+
+    public function __construct(OutputInterface $output = null)
+    {
+        if ($output === null) {
+            $output = \in_array(\PHP_SAPI, ['cli', 'phpdbg', 'embed'], true) ? new CliOutput() : new HtmlOutput();
+        }
+        $this->output = $output;
+    }
+
+    public function dump(int $backtraceLimit = 10, int $offset = 1, array $traces = []): void
+    {
+        if ($backtraceLimit <= 0) {
+            $backtraceLimit = 1;
+        }
+
+        if ($offset <= 0) {
+            $offset = 0;
+        }
+
+        if (empty($traces)) {
+            $traces = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, $backtraceLimit);
+        }
+        $traces = array_slice($traces, $offset, $backtraceLimit, false);
+        $this->output->print(array_reverse($traces));
+    }
+
+}

+ 61 - 0
src/Output/BacktraceOutput/CliOutput.php

@@ -0,0 +1,61 @@
+<?php
+
+namespace Michel\Debug\Output\BacktraceOutput;
+
+use Michel\Debug\Output\OutputInterface;
+
+final class CliOutput implements OutputInterface
+{
+
+    /**
+     * @var callable|null
+     */
+    private $output;
+
+    public function __construct(callable $output = null)
+    {
+        if ($output === null) {
+            $output = function (string $dumped) {
+                fwrite(STDOUT, $dumped);
+            };
+        }
+        $this->output = $output;
+    }
+
+    public function print($value): void
+    {
+        if (!is_array($value)) {
+            return;
+        }
+
+        $out[] = sprintf("Backtrace (last %d calls):".PHP_EOL.PHP_EOL, count($value));
+
+        $maxWidth = $this->geTerminalWidth() - 12;
+        foreach ($value as $i => $entry) {
+            $file = $entry['file'] ?? '[internal]';
+            $line = $entry['line'] ?? '-';
+            $function = ($entry['class'] ?? '') . ($entry['type'] ?? '') . $entry['function'];
+
+            $file = self::truncateLeft($file, $maxWidth);
+            $function = self::truncateLeft($function, $maxWidth);
+
+            $out[]  = sprintf("#%d", $i + 1);
+            $out[]  = sprintf("  File    : %s:%s", $file, $line);
+            $out[]  = "  Call    : $function";
+        }
+
+        $outputCallback = $this->output;
+        $outputCallback(implode(PHP_EOL, $out).PHP_EOL);
+    }
+
+    private static function truncateLeft(string $text, int $maxLength): string {
+        return strlen($text) > $maxLength
+            ? '...' . substr($text, -($maxLength - 3))
+            : $text;
+    }
+
+    private function geTerminalWidth(): int
+    {
+        return ((int)exec('tput cols') ?? 85 - 5);
+    }
+}

+ 56 - 0
src/Output/BacktraceOutput/HtmlOutput.php

@@ -0,0 +1,56 @@
+<?php
+
+namespace Michel\Debug\Output\BacktraceOutput;
+
+use Michel\Debug\Output\OutputInterface;
+
+final class HtmlOutput implements OutputInterface
+{
+    private $output;
+
+    public function __construct(callable $output = null)
+    {
+        if ($output === null) {
+            $output = function (string $dumped) {
+                echo $dumped;
+            };
+        }
+        $this->output = $output;
+    }
+
+    public function print($value): void
+    {
+        if (!is_array($value)) {
+            return;
+        }
+
+        $html[] = '<style>';
+        $html[] = file_get_contents(dirname(__DIR__, 3) . '/resources/css/backtrace.css');
+        $html[] = '</style>';
+
+        $html[] = '<div class="__beautify-backtrace-container">';
+        $html[] = sprintf(
+            '<div class="__beautify-backtrace-title">Backtrace — last %d call(s)</div>' . PHP_EOL . PHP_EOL,
+            count($value)
+        );
+
+        foreach ($value as $i => $entry) {
+            $file = $entry['file'] ?? '[internal]';
+            $line = $entry['line'] ?? '-';
+            $function = ($entry['class'] ?? '') . ($entry['type'] ?? '') . $entry['function'];
+
+            $html[] = sprintf(
+                '<div class="__beautify-backtrace-dumper">#%d - %s: Called from %s:%s</div>',
+                $i + 1,
+                htmlspecialchars($function, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'),
+                htmlspecialchars($file, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'),
+                htmlspecialchars($line, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8')
+            );
+        }
+
+        $html[] = '</div>';
+
+        $outputCallback = $this->output;
+        $outputCallback(implode('', $html));
+    }
+}

+ 8 - 0
src/Output/OutputInterface.php

@@ -0,0 +1,8 @@
+<?php
+
+namespace Michel\Debug\Output;
+
+interface OutputInterface
+{
+    public function print($value): void;
+}

+ 84 - 0
src/Output/VarDumperOutput/CliPrintOutput.php

@@ -0,0 +1,84 @@
+<?php
+
+namespace Michel\Debug\Output\VarDumperOutput;
+
+use Michel\Debug\Output\OutputInterface;
+use Michel\Debug\Util\ObjectPropertyExtractor;
+use Michel\Debug\Util\DataDescriptor;
+
+final class CliPrintOutput implements OutputInterface
+{
+
+    private int $maxDepth;
+    /**
+     * @var callable|null
+     */
+    private $output;
+
+    public function __construct(int $maxDepth = 5, callable $output = null)
+    {
+        if ($maxDepth <= 0) {
+            $maxDepth = 5;
+        }
+        $this->maxDepth = $maxDepth;
+        if ($output === null) {
+            $output = function (string $dumped) {
+                fwrite(STDOUT, $dumped);
+            };
+        }
+        $this->output = $output;
+    }
+
+    public function print($value): void
+    {
+        $description = DataDescriptor::describe($value, 0, $this->maxDepth);
+        $fragments = $this->renderValueNode($description);
+        $dumped = rtrim(implode('', $fragments), PHP_EOL).PHP_EOL;
+        $outputCallback = $this->output;
+        $outputCallback($dumped);
+    }
+
+
+    private function renderValueNode($node): array
+    {
+        $pad = '';
+        $indent = $node['depth'];
+        $value = $node['value'];
+        $type = $node['type'];
+
+        if ($indent) {
+            $pad = str_repeat(' ', $indent * 2);
+        }
+        $out = [];
+        if ($node['truncated']) {
+            $out[] = $value;
+            return $out;
+        }
+
+        switch ($type) {
+            case 'NULL':
+                $out[] = 'null';
+                return $out;
+            case 'array':
+                $out[] = sprintf('%s ['. PHP_EOL, $value);
+                foreach ($node['items'] as $k => $description) {
+                    $out[] = $pad . "  [$k] => " . implode('', $this->renderValueNode($description)) . PHP_EOL;
+                }
+                $out[] = $pad . ']';
+                return $out;
+            case 'object':
+                $out[] = sprintf('%s {'. PHP_EOL, $value);
+                foreach ($node['properties'] as $k => $description) {
+                    $k = sprintf('%s::%s', $node['class'], $k);
+                    $out[] = $pad . "  [$k] => " . implode('', $this->renderValueNode($description)) . PHP_EOL;
+                }
+                $out[] = $pad . "}";
+                return $out;
+            default:
+                $out[] = sprintf('(%s) %s', $type, $value);
+                return $out;
+        }
+
+    }
+
+}

+ 36 - 0
src/Output/VarDumperOutput/CliVarDumpOutput.php

@@ -0,0 +1,36 @@
+<?php
+
+namespace Michel\Debug\Output\VarDumperOutput;
+
+use Michel\Debug\Output\OutputInterface;
+
+final class CliVarDumpOutput implements OutputInterface
+{
+
+    /**
+     * @var callable|null
+     */
+    private $output;
+
+    public function __construct(callable $output = null)
+    {
+        if ($output === null) {
+            $output = function (string $dumped) {
+                fwrite(STDOUT, $dumped);
+            };
+        }
+        $this->output = $output;
+    }
+
+    public function print($value): void
+    {
+        ob_start();
+        var_dump($value);
+        $dumped = ob_get_clean();
+        if ($dumped === false) {
+            throw new \RuntimeException('Failed to dump the provided value using var_dump.');
+        }
+        $output = $this->output;
+        $output($dumped);
+    }
+}

+ 44 - 0
src/Output/VarDumperOutput/ConsoleLogOutput.php

@@ -0,0 +1,44 @@
+<?php
+
+namespace Michel\Debug\Output\VarDumperOutput;
+
+use Michel\Debug\Output\OutputInterface;
+
+final class ConsoleLogOutput implements OutputInterface
+{
+    /**
+     * @var callable|null
+     */
+    private $output;
+
+    public function __construct(callable $output = null)
+    {
+        if ($output === null) {
+            $output = function (string $dumped) {
+                echo $dumped;
+            };
+        }
+        $this->output = $output;
+    }
+
+    public function print($value): void
+    {
+        if ($this->isCli()) {
+            (new CliPrintOutput())->print($value);
+            return;
+        }
+        $html[] = '<script>';
+        $js = file_get_contents(dirname(__DIR__, 3) . '/resources/js/console.log.js');
+        $html[] = str_replace('[value_to_debug]', json_encode(print_r($value, true)), $js);
+        $html[] = '</script>';
+
+        $dumped = implode('', $html);
+        $output = $this->output;
+        $output($dumped);
+    }
+
+    private function isCli(): bool
+    {
+        return \in_array(\PHP_SAPI, ['cli', 'phpdbg', 'embed'], true);
+    }
+}

+ 134 - 0
src/Output/VarDumperOutput/HtmlOutput.php

@@ -0,0 +1,134 @@
+<?php
+
+namespace Michel\Debug\Output\VarDumperOutput;
+
+use Michel\Debug\Output\OutputInterface;
+use Michel\Debug\Util\ObjectPropertyExtractor;
+use Michel\Debug\Util\DataDescriptor;
+use ReflectionClass;
+use ReflectionProperty;
+
+final class HtmlOutput implements OutputInterface
+{
+
+    private int $maxDepth;
+    /**
+     * @var callable|null
+     */
+    private $output;
+
+    public function __construct(int $maxDepth = 5, callable $output = null)
+    {
+        if ($maxDepth <= 0) {
+            $maxDepth = 5;
+        }
+        $this->maxDepth = $maxDepth;
+        if ($output === null) {
+            $output = function (string $dumped) {
+                echo $dumped;
+            };
+        }
+        $this->output = $output;
+    }
+
+    public function print($value): void
+    {
+        $id ='var_dump_'.md5(uniqid());
+
+        $html[] = '<style>';
+        $css  = file_get_contents(dirname(__DIR__, 3) . '/resources/css/dump.css');
+        $html[] = str_replace('#uniqId', "#$id", $css);
+        $html[] = '</style>';
+
+        $html[] = '<script>';
+        $js = file_get_contents(dirname(__DIR__, 3) . '/resources/js/debug.js');
+        $js = str_replace('#uniqId', "#$id", $js);
+        $html[] = $js;
+        $html[] = '</script>';
+
+        $html[] = sprintf('<div id="%s" class="__beautify-var-dumper">', $id);
+
+        $description = DataDescriptor::describe($value, 0, $this->maxDepth);
+        $fragments = $this->renderValueNode($description);
+        $it = new \RecursiveIteratorIterator(new \RecursiveArrayIterator($fragments));
+        foreach ($it as $v) {
+            $html[] = $v;
+        }
+        $html[] = '</div>';
+        $dumped = implode('', $html);
+
+        $outputCallback = $this->output;
+        $outputCallback($dumped);
+    }
+
+    private function renderValueNode($node): array
+    {
+        $html = [];
+        $indent = $node['depth'];
+        $indentStr = str_repeat('&nbsp;', $indent * 2);
+        if ($node['truncated']) {
+            $html[] = "<div>{$indentStr}{$node['value']}</div>";
+            return $html;
+        }
+
+        $value = $node['value'];
+        $type = $node['type'];
+        switch ($type) {
+            case 'NULL':
+                $length = $node['length'] ?? null;
+                $html[] = "<span class='$type'>$value</span>";
+                break;
+            case 'string':
+            case 'int':
+            case 'float':
+            case 'bool':
+            case 'boolean':
+                $length = $node['length'] ?? null;
+                $html[] = "<span class='$type'><span class='type'>$type</span> $value</span>";
+                if ($length) {
+                    $html[] = " <small><i>(Lenght: $length)</i></small>";
+                }
+                break;
+            case 'array':
+                $id = 'target_'.md5(uniqid());
+                $count = $node['count'];
+                $caretValue =  sprintf(
+                    "data-target='#%s'>array</span> <small><i>(Size: %d)</i></small> (<br>",
+                    $id,
+                    $count
+                );
+                if ($indent <= 1) {
+                    $html[] = "<span class='type caret caret-down' $caretValue";
+                    $html[] = "<div class='nested active' id='{$id}'>";
+                }else {
+                    $html[] = "<span class='type caret' $caretValue";
+                    $html[] = "<div class='nested' id='{$id}'>";
+                }
+                foreach ($node['items'] as $key => $description) {
+                    if (is_string($key)) {
+                        $key = sprintf('"%s"', $key);
+                    }
+                    $html[] = "{$indentStr}  <span class='key'>$key</span> => ";
+                    $html[] = $this->renderValueNode($description);
+                }
+                $html[] = "{$indentStr})";
+                $html[] = "</div>";
+                return $html;
+            case 'object':
+                $html[] =  "<span class='type'>object</span> $value {<br>";
+                foreach ($node['properties'] as $key => $description) {
+                    $key = sprintf('%s::%s', $node['class'], $key);
+                    $html[] = "{$indentStr}  <span class='key'>$key</span> => ";
+                    $html[] =  $this->renderValueNode($description);
+                }
+                $html[] = "{$indentStr}}";
+                break;
+            default:
+                $html[] = "<span class='$type'><span class='type'>$type</span> $value</span>";
+        }
+
+        $html[] = "<br>";
+        return $html;
+    }
+
+}

+ 79 - 0
src/Util/DataDescriptor.php

@@ -0,0 +1,79 @@
+<?php
+
+namespace Michel\Debug\Util;
+
+class DataDescriptor
+{
+    public static function describe($value, int $depth = 0, int $maxDepth = 5): array
+    {
+        if ($depth >= $maxDepth) {
+            return [
+                'type' => gettype($value),
+                'depth' => $depth,
+                'truncated' => true,
+                'value' => '…'
+            ];
+        }
+
+        $type = gettype($value);
+        $result = [
+            'type' => $type,
+            'depth' => $depth,
+            'truncated' => false
+        ];
+
+        switch (true) {
+            case is_null($value):
+                $result['value'] = 'null';
+                break;
+
+            case is_bool($value):
+                $result['value'] = $value ? 'true' : 'false';
+                break;
+
+            case is_float($value):
+                $result['type'] = 'float';
+                $result['value'] = $value;
+                break;
+
+            case is_int($value):
+                $result['type'] = 'int';
+                $result['value'] = $value;
+                break;
+
+            case is_string($value):
+                $result['length'] = function_exists('mb_strlen') ? mb_strlen($value, 'UTF-8') : strlen($value);
+                $result['value'] = sprintf('"%s"', $value);
+                break;
+
+            case is_array($value):
+                $result['count'] = count($value);
+                $result['value'] = sprintf('array(%d)', $result['count']);       // ← affichable, homogène
+                $result['items'] = [];
+                foreach ($value as $k => $v) {
+                    $result['items'][$k] = self::describe($v, $depth + 1, $maxDepth); // ← depth++
+                }
+                break;
+
+            case is_object($value):
+                $class = get_class($value);
+                $result['class'] = $class;
+                $result['object_id'] = spl_object_id($value);
+                $result['value'] = sprintf('%s#%d', $class, $result['object_id']);
+                $result['properties'] = [];
+                foreach (ObjectPropertyExtractor::extract($value) as $prop => $val) {
+                    $result['properties'][$prop] = self::describe($val, $depth + 1, $maxDepth); // ← depth++
+                }
+                break;
+
+            case is_resource($value):
+                $result['value'] = sprintf('resource(%s)', get_resource_type($value));
+                break;
+            default:
+                $result['value'] = (string)$value;
+        }
+
+
+        return $result;
+    }
+}

+ 50 - 0
src/Util/ObjectPropertyExtractor.php

@@ -0,0 +1,50 @@
+<?php
+
+namespace Michel\Debug\Util;
+
+final class ObjectPropertyExtractor
+{
+
+    public static function extract(object  $object): array
+    {
+        $reflection = new \ReflectionClass($object);
+        $properties = [];
+        $parentClass = $reflection->getParentClass();
+        while ($parentClass) {
+            foreach ($parentClass->getProperties(\ReflectionProperty::IS_PRIVATE | \ReflectionProperty::IS_PROTECTED) as $property) {
+                $property->setAccessible(true);
+                $properties[$property->getName()] = $property->getValue($object);
+            }
+            $parentClass = $parentClass->getParentClass();
+        }
+        foreach ($reflection->getTraits() as $trait) {
+            foreach ($trait->getProperties() as $property) {
+                $property = $reflection->getProperty($property->getName());
+                $property->setAccessible(true);
+                $properties[$property->getName()] = $property->getValue($object);
+            }
+        }
+
+        foreach (get_object_vars($object) as $name => $value) {
+            $properties[$name] = $value;
+        }
+
+        foreach ($reflection->getProperties() as $property) {
+            $property->setAccessible(true);
+            $properties[$property->getName()] = $property->getValue($object);
+        }
+        $result = [];
+        foreach ($properties as $key => $value) {
+            $result[$key] = $value;
+        }
+
+        if ($reflection->hasMethod('__debugInfo')) {
+            $method = $reflection->getMethod('__debugInfo');
+            $infos = $method->invoke($object);
+            foreach ($infos as $key => $value) {
+                $result[$key] = $value;
+            }
+        }
+        return $result;
+    }
+}

+ 27 - 0
src/VarDumper.php

@@ -0,0 +1,27 @@
+<?php
+
+namespace Michel\Debug;
+
+use Michel\Debug\Output\OutputInterface;
+use Michel\Debug\Output\VarDumperOutput\CliPrintOutput;
+use Michel\Debug\Output\VarDumperOutput\HtmlOutput;
+
+final class VarDumper
+{
+    private OutputInterface $output;
+
+    public function __construct(OutputInterface $output = null)
+    {
+        if ($output === null) {
+            $output = \in_array(\PHP_SAPI, ['cli', 'phpdbg', 'embed'], true) ? new CliPrintOutput() : new HtmlOutput();
+        }
+        $this->output = $output;
+    }
+
+    public function dump(...$vars): void
+    {
+        foreach ($vars as $item) {
+           $this->output->print($item);
+        }
+    }
+}

+ 86 - 0
tests/BacktraceDumperTest.php

@@ -0,0 +1,86 @@
+<?php
+
+namespace Test\Michel\Debug;
+
+use Michel\Debug\BacktraceDumper;
+use Michel\Debug\Output\BacktraceOutput\CliOutput;
+use Michel\Debug\VarDumper;
+use Michel\UniTester\TestCase;
+
+class BacktraceDumperTest extends TestCase
+{
+
+    protected function setUp(): void
+    {
+        // TODO: Implement setUp() method.
+    }
+
+    protected function tearDown(): void
+    {
+        // TODO: Implement tearDown() method.
+    }
+
+    protected function execute(): void
+    {
+        $fakeTrace = [
+            [
+                'file' => '/var/www/app/src/Controller/HomeController.php',
+                'line' => 42,
+                'function' => 'indexAction',
+                'class' => 'App\\Controller\\HomeController',
+                'object' => (object) [],
+                'type' => '->',
+                'args' => [
+                    ['type' => 'string', 'value' => 'Hello world'],
+                    ['type' => 'int', 'value' => 123],
+                ],
+            ],
+            [
+                'file' => '/var/www/app/vendor/symfony/http-kernel/HttpKernel.php',
+                'line' => 158,
+                'function' => 'handleRaw',
+                'class' => 'Symfony\\Component\\HttpKernel\\HttpKernel',
+                'object' => (object) [],
+                'type' => '->',
+                'args' => [],
+            ],
+            [
+                'file' => '/var/www/app/vendor/symfony/http-kernel/HttpKernel.php',
+                'line' => 80,
+                'function' => 'handle',
+                'class' => 'Symfony\\Component\\HttpKernel\\HttpKernel',
+                'object' => (object) [],
+                'type' => '->',
+                'args' => [
+                    ['type' => 'string', 'value' => 'prod'],
+                    ['type' => 'bool', 'value' => true],
+                ],
+            ],
+            [
+                'file' => '/var/www/app/public/index.php',
+                'line' => 25,
+                'function' => '{closure}',
+                'args' => [],
+            ],
+            [
+                'file' => '/var/www/app/public/index.php',
+                'line' => 15,
+                'function' => 'require_once',
+                'args' => ['/var/www/app/config/bootstrap.php'],
+            ],
+        ];
+
+        $output = new CliOutput(function ($dumped) use($fakeTrace) {
+            $this->assertEquals(base64_encode($dumped), 'QmFja3RyYWNlIChsYXN0IDUgY2FsbHMpOgoKCiMxCiAgRmlsZSAgICA6IC92YXIvd3d3L2FwcC9wdWJsaWMvaW5kZXgucGhwOjE1CiAgQ2FsbCAgICA6IHJlcXVpcmVfb25jZQojMgogIEZpbGUgICAgOiAvdmFyL3d3dy9hcHAvcHVibGljL2luZGV4LnBocDoyNQogIENhbGwgICAgOiB7Y2xvc3VyZX0KIzMKICBGaWxlICAgIDogL3Zhci93d3cvYXBwL3ZlbmRvci9zeW1mb255L2h0dHAta2VybmVsL0h0dHBLZXJuZWwucGhwOjgwCiAgQ2FsbCAgICA6IFN5bWZvbnlcQ29tcG9uZW50XEh0dHBLZXJuZWxcSHR0cEtlcm5lbC0+aGFuZGxlCiM0CiAgRmlsZSAgICA6IC92YXIvd3d3L2FwcC92ZW5kb3Ivc3ltZm9ueS9odHRwLWtlcm5lbC9IdHRwS2VybmVsLnBocDoxNTgKICBDYWxsICAgIDogU3ltZm9ueVxDb21wb25lbnRcSHR0cEtlcm5lbFxIdHRwS2VybmVsLT5oYW5kbGVSYXcKIzUKICBGaWxlICAgIDogL3Zhci93d3cvYXBwL3NyYy9Db250cm9sbGVyL0hvbWVDb250cm9sbGVyLnBocDo0MgogIENhbGwgICAgOiBBcHBcQ29udHJvbGxlclxIb21lQ29udHJvbGxlci0+aW5kZXhBY3Rpb24K');
+        });
+        $varDumper = new BacktraceDumper($output);
+        $varDumper->dump(10, 0, $fakeTrace);
+
+
+        $output = new CliOutput(function ($dumped) use($fakeTrace) {
+            $this->assertEquals(base64_encode($dumped), "QmFja3RyYWNlIChsYXN0IDIgY2FsbHMpOgoKCiMxCiAgRmlsZSAgICA6IC92YXIvd3d3L2FwcC92ZW5kb3Ivc3ltZm9ueS9odHRwLWtlcm5lbC9IdHRwS2VybmVsLnBocDoxNTgKICBDYWxsICAgIDogU3ltZm9ueVxDb21wb25lbnRcSHR0cEtlcm5lbFxIdHRwS2VybmVsLT5oYW5kbGVSYXcKIzIKICBGaWxlICAgIDogL3Zhci93d3cvYXBwL3NyYy9Db250cm9sbGVyL0hvbWVDb250cm9sbGVyLnBocDo0MgogIENhbGwgICAgOiBBcHBcQ29udHJvbGxlclxIb21lQ29udHJvbGxlci0+aW5kZXhBY3Rpb24K");
+        });
+        $varDumper = new BacktraceDumper($output);
+        $varDumper->dump(2, 0, $fakeTrace);
+    }
+}

+ 36 - 0
tests/CliPrintOutputTest.php

@@ -0,0 +1,36 @@
+<?php
+
+namespace Test\Michel\Debug;
+
+use Michel\Debug\Output\VarDumperOutput\CliPrintOutput;
+use Michel\UniTester\TestCase;
+
+class CliPrintOutputTest extends TestCase
+{
+
+    protected function setUp(): void
+    {
+        // TODO: Implement setUp() method.
+    }
+
+    protected function tearDown(): void
+    {
+        // TODO: Implement tearDown() method.
+    }
+
+    protected function execute(): void
+    {
+       $this->testPrint();
+    }
+
+    public function testPrint()
+    {
+        ob_start();
+        $cliOutput = new CliPrintOutput(5, function ($dumped) {
+            echo $dumped;
+        });
+        $cliOutput->print("Hello, world!");
+        $output = ob_get_clean();
+        $this->assertEquals('(string) "Hello, world!"'.PHP_EOL, $output);
+    }
+}

+ 36 - 0
tests/CliVarDumpOutputTest.php

@@ -0,0 +1,36 @@
+<?php
+
+namespace Test\Michel\Debug;
+
+use Michel\Debug\Output\VarDumperOutput\CliVarDumpOutput;
+use Michel\UniTester\TestCase;
+
+class CliVarDumpOutputTest extends TestCase
+{
+
+    protected function setUp(): void
+    {
+        // TODO: Implement setUp() method.
+    }
+
+    protected function tearDown(): void
+    {
+        // TODO: Implement tearDown() method.
+    }
+
+    protected function execute(): void
+    {
+       $this->testVarDump();
+    }
+
+    public function testVarDump()
+    {
+        ob_start();
+        $cliOutput = new CliVarDumpOutput(function ($dumped) {
+            echo $dumped;
+        });
+        $cliOutput->print("Hello, world!");
+        $output = ob_get_clean();
+        $this->assertStringContains($output, 'string(13) "Hello, world!"');
+    }
+}

+ 35 - 0
tests/HtmlOutputTest.php

@@ -0,0 +1,35 @@
+<?php
+
+namespace Test\Michel\Debug;
+
+use Michel\Debug\Output\VarDumperOutput\HtmlOutput;
+use Michel\UniTester\TestCase;
+
+class HtmlOutputTest extends TestCase
+{
+
+    protected function setUp(): void
+    {
+        // TODO: Implement setUp() method.
+    }
+
+    protected function tearDown(): void
+    {
+        // TODO: Implement tearDown() method.
+    }
+
+    protected function execute(): void
+    {
+        $this->testInspectItem();
+    }
+
+    public function testInspectItem(): void
+    {
+        $output = function (string $dumped) {
+            $this->assertTrue(strip_tags($dumped) !== $dumped);
+        };
+        $htmlOutput = new HtmlOutput(5, $output);
+        $htmlOutput->print(['key' => 'value']);
+    }
+
+}

+ 322 - 0
tests/StructuredDataDescriptorTest.php

@@ -0,0 +1,322 @@
+<?php
+
+namespace Test\Michel\Debug;
+
+use Michel\Debug\Util\DataDescriptor;
+use Michel\UniTester\TestCase;
+
+class StructuredDataDescriptorTest extends TestCase
+{
+
+    protected function setUp(): void
+    {
+        // TODO: Implement setUp() method.
+    }
+
+    protected function tearDown(): void
+    {
+        // TODO: Implement tearDown() method.
+    }
+
+    protected function execute(): void
+    {
+        $result = DataDescriptor::describe([
+            'name' => 'John Doe',
+            'email' => 'john.doe@example.com',
+            'active' => true,
+            'roles' => ['admin', 'user'],
+            'age' => 30
+        ]);
+
+        $this->assertEquals($result, array(
+            'type' => 'array',
+            'depth' => 0,
+            'truncated' => false,
+            'count' => 5,
+            'value' => 'array(5)',
+            'items' =>
+                array(
+                    'name' =>
+                        array(
+                            'type' => 'string',
+                            'depth' => 1,
+                            'truncated' => false,
+                            'length' => 8,
+                            'value' => '"John Doe"',
+                        ),
+                    'email' =>
+                        array(
+                            'type' => 'string',
+                            'depth' => 1,
+                            'truncated' => false,
+                            'length' => 20,
+                            'value' => '"john.doe@example.com"',
+                        ),
+                    'active' =>
+                        array(
+                            'type' => 'boolean',
+                            'depth' => 1,
+                            'truncated' => false,
+                            'value' => 'true',
+                        ),
+                    'roles' =>
+                        array(
+                            'type' => 'array',
+                            'depth' => 1,
+                            'truncated' => false,
+                            'count' => 2,
+                            'value' => 'array(2)',
+                            'items' =>
+                                array(
+                                    0 =>
+                                        array(
+                                            'type' => 'string',
+                                            'depth' => 2,
+                                            'truncated' => false,
+                                            'length' => 5,
+                                            'value' => '"admin"',
+                                        ),
+                                    1 =>
+                                        array(
+                                            'type' => 'string',
+                                            'depth' => 2,
+                                            'truncated' => false,
+                                            'length' => 4,
+                                            'value' => '"user"',
+                                        ),
+                                ),
+                        ),
+                    'age' =>
+                        array(
+                            'type' => 'int',
+                            'depth' => 1,
+                            'truncated' => false,
+                            'value' => 30,
+                        ),
+                ),
+        ));
+
+        $result = DataDescriptor::describe([
+            'string' => 'Hello world',
+            'int' => 42,
+            'float' => 3.14,
+            'boolTrue' => true,
+            'boolFalse' => false,
+            'nullValue' => null,
+            'arraySimple' => [1, 2, 3],
+            'arrayNested' => [
+                'level1' => [
+                    'level2' => [
+                        'level3a' => 'deep',
+                        'level3b' => [4, 5, 6]
+                    ],
+                    'level2b' => 'mid'
+                ],
+                'anotherKey' => 'value'
+            ],
+            'objectSimple' => (object)['foo' => 'bar', 'baz' => 123],
+        ]);
+
+        $this->assertEquals($result, array(
+            'type' => 'array',
+            'depth' => 0,
+            'truncated' => false,
+            'count' => 9,
+            'value' => 'array(9)',
+            'items' =>
+                array(
+                    'string' =>
+                        array(
+                            'type' => 'string',
+                            'depth' => 1,
+                            'truncated' => false,
+                            'length' => 11,
+                            'value' => '"Hello world"',
+                        ),
+                    'int' =>
+                        array(
+                            'type' => 'int',
+                            'depth' => 1,
+                            'truncated' => false,
+                            'value' => 42,
+                        ),
+                    'float' =>
+                        array(
+                            'type' => 'float',
+                            'depth' => 1,
+                            'truncated' => false,
+                            'value' => 3.14,
+                        ),
+                    'boolTrue' =>
+                        array(
+                            'type' => 'boolean',
+                            'depth' => 1,
+                            'truncated' => false,
+                            'value' => 'true',
+                        ),
+                    'boolFalse' =>
+                        array(
+                            'type' => 'boolean',
+                            'depth' => 1,
+                            'truncated' => false,
+                            'value' => 'false',
+                        ),
+                    'nullValue' =>
+                        array(
+                            'type' => 'NULL',
+                            'depth' => 1,
+                            'truncated' => false,
+                            'value' => 'null',
+                        ),
+                    'arraySimple' =>
+                        array(
+                            'type' => 'array',
+                            'depth' => 1,
+                            'truncated' => false,
+                            'count' => 3,
+                            'value' => 'array(3)',
+                            'items' =>
+                                array(
+                                    0 =>
+                                        array(
+                                            'type' => 'int',
+                                            'depth' => 2,
+                                            'truncated' => false,
+                                            'value' => 1,
+                                        ),
+                                    1 =>
+                                        array(
+                                            'type' => 'int',
+                                            'depth' => 2,
+                                            'truncated' => false,
+                                            'value' => 2,
+                                        ),
+                                    2 =>
+                                        array(
+                                            'type' => 'int',
+                                            'depth' => 2,
+                                            'truncated' => false,
+                                            'value' => 3,
+                                        ),
+                                ),
+                        ),
+                    'arrayNested' =>
+                        array(
+                            'type' => 'array',
+                            'depth' => 1,
+                            'truncated' => false,
+                            'count' => 2,
+                            'value' => 'array(2)',
+                            'items' =>
+                                array(
+                                    'level1' =>
+                                        array(
+                                            'type' => 'array',
+                                            'depth' => 2,
+                                            'truncated' => false,
+                                            'count' => 2,
+                                            'value' => 'array(2)',
+                                            'items' =>
+                                                array(
+                                                    'level2' =>
+                                                        array(
+                                                            'type' => 'array',
+                                                            'depth' => 3,
+                                                            'truncated' => false,
+                                                            'count' => 2,
+                                                            'value' => 'array(2)',
+                                                            'items' =>
+                                                                array(
+                                                                    'level3a' =>
+                                                                        array(
+                                                                            'type' => 'string',
+                                                                            'depth' => 4,
+                                                                            'truncated' => false,
+                                                                            'length' => 4,
+                                                                            'value' => '"deep"',
+                                                                        ),
+                                                                    'level3b' =>
+                                                                        array(
+                                                                            'type' => 'array',
+                                                                            'depth' => 4,
+                                                                            'truncated' => false,
+                                                                            'count' => 3,
+                                                                            'value' => 'array(3)',
+                                                                            'items' =>
+                                                                                array(
+                                                                                    0 =>
+                                                                                        array(
+                                                                                            'type' => 'integer',
+                                                                                            'depth' => 5,
+                                                                                            'truncated' => true,
+                                                                                            'value' => '…',
+                                                                                        ),
+                                                                                    1 =>
+                                                                                        array(
+                                                                                            'type' => 'integer',
+                                                                                            'depth' => 5,
+                                                                                            'truncated' => true,
+                                                                                            'value' => '…',
+                                                                                        ),
+                                                                                    2 =>
+                                                                                        array(
+                                                                                            'type' => 'integer',
+                                                                                            'depth' => 5,
+                                                                                            'truncated' => true,
+                                                                                            'value' => '…',
+                                                                                        ),
+                                                                                ),
+                                                                        ),
+                                                                ),
+                                                        ),
+                                                    'level2b' =>
+                                                        array(
+                                                            'type' => 'string',
+                                                            'depth' => 3,
+                                                            'truncated' => false,
+                                                            'length' => 3,
+                                                            'value' => '"mid"',
+                                                        ),
+                                                ),
+                                        ),
+                                    'anotherKey' =>
+                                        array(
+                                            'type' => 'string',
+                                            'depth' => 2,
+                                            'truncated' => false,
+                                            'length' => 5,
+                                            'value' => '"value"',
+                                        ),
+                                ),
+                        ),
+                    'objectSimple' =>
+                        array(
+                            'type' => 'object',
+                            'depth' => 1,
+                            'truncated' => false,
+                            'class' => 'stdClass',
+                            'object_id' => 13,
+                            'value' => 'stdClass#13',
+                            'properties' =>
+                                array(
+                                    'foo' =>
+                                        array(
+                                            'type' => 'string',
+                                            'depth' => 2,
+                                            'truncated' => false,
+                                            'length' => 3,
+                                            'value' => '"bar"',
+                                        ),
+                                    'baz' =>
+                                        array(
+                                            'type' => 'int',
+                                            'depth' => 2,
+                                            'truncated' => false,
+                                            'value' => 123,
+                                        ),
+                                ),
+                        ),
+                ),
+        ));
+    }
+}

+ 30 - 0
tests/VarDumperTest.php

@@ -0,0 +1,30 @@
+<?php
+
+namespace Test\Michel\Debug;
+
+use Michel\Debug\Output\VarDumperOutput\CliPrintOutput;
+use Michel\Debug\VarDumper;
+use Michel\UniTester\TestCase;
+
+class VarDumperTest extends TestCase
+{
+
+    protected function setUp(): void
+    {
+        // TODO: Implement setUp() method.
+    }
+
+    protected function tearDown(): void
+    {
+        // TODO: Implement tearDown() method.
+    }
+
+    protected function execute(): void
+    {
+        $output = new CliPrintOutput(5, function ($dumped) {
+            $this->assertEquals('(string) "foo"'.PHP_EOL, $dumped);
+        });
+        $varDumper = new VarDumper($output);
+        $varDumper->dump('foo');
+    }
+}