php basic debugging with var_dump and print_r

Part of the course: php for beginners

PHP Basic Debugging with var_dump() and print_r()

Introduction to PHP Debugging

This chapter gives you a clear, practical grounding in debugging for PHP — what it is, why simple tools like var_dump() and print_r() are valuable, and when it’s appropriate to rely on simple output debugging versus more advanced techniques.

What is debugging?

Debugging is the process of finding, understanding, and fixing problems (bugs) in your code. A “bug” can be anything from a syntax error that prevents code from running, to a logical mistake that produces incorrect results, to unexpected runtime behavior (exceptions, warnings, wrong data types, etc.).

Debugging has three basic steps:

  1. Observe: Notice symptoms (errors, warnings, wrong output, slow performance).

  2. Inspect: Examine the program state — variable values, data structures, program flow.

  3. Fix & Verify: Change the code to resolve the issue, then rerun tests or inspect again to ensure the bug is gone.

Good debugging reduces guesswork: the faster you can inspect the actual state of your application, the faster you can find the root cause.

Why simple debugging tools matter

Simple output-based tools (var_dump(), print_r(), echo, printf) are the most immediate, low-overhead way to inspect what your program is doing. They’re important because:

  • Immediate feedback: You add a single line and instantly see values/types in the output. No setup required.

  • Universally available: They work on any PHP setup — command line, shared hosting, or local dev — without installing extensions or configuring an IDE.

  • Great for quick checks: For checking a variable’s value, type, or small data structure, these functions are faster than setting up breakpoints.

  • Explain complex structures: var_dump() shows types and lengths; print_r() prints readable array/object structures — both help understand nested data.

  • Useful in constrained environments: When you cannot attach a debugger (e.g., remote server with limited access), simple output is often the only option.

However, they aren’t a replacement for proper testing and advanced debugging: they’re a practical first step to inspect program state and narrow down problems.

When to use basic output debugging

Use output debugging in these situations:

1. Quick sanity checks

If something looks wrong (unexpected value, empty array), add one var_dump($x) or print_r($x, true) to see what’s there. Example:

$array = fetchData();
var_dump($array); // quick inspect

2. During initial development or prototyping

When you’re iterating quickly and want to verify data flows — forms, API responses, or database queries — simple prints help you confirm assumptions.

3. When debugging small, isolated problems

If you can reduce the issue to a small routine or a single request, printing values at key points is faster than setting up a step-through debugger.

4. When the environment limits other tools

On shared hosting or production systems where installing Xdebug or connecting an IDE is difficult, printing/logging is practical.

5. Logging for later inspection

You can capture printed output and save it to logs (error_log() or file writes) to analyze after the fact, especially for intermittent issues.

When to avoid output debugging

There are times where basic output debugging is not appropriate:

  • Complex, multi-threaded, or asynchronous flows: Tracing execution across many components is hard with prints.

  • Performance-sensitive code: Leaving dump statements can slow things and bloat logs.

  • Production environments: Printing sensitive data to users or logs risks leaking secrets and harming UX.

  • Hard-to-reproduce bugs: Race conditions or timing issues often require proper debuggers, profilers, or logging with timestamps.

Practical tips for using output debugging effectively

  • Wrap output in <pre> tags when debugging HTML pages so output is readable:

    echo '<pre>';
    var_dump($data);
    echo '</pre>';
  • Add context to your dumps so you know where they came from:

    echo "<pre>At line 42, \$user:\n";
    var_dump($user);
    echo "</pre>";
  • Use print_r($var, true) to get a string you can log rather than echo.

  • Avoid committing debug statements: Use TODO comments or feature flags to track temporary debug code.

  • Combine with logging: For production-safe debugging, write structured logs rather than dumping to output.

Understanding var_dump()

This section gives a complete, practical explanation of PHP’s var_dump() — what it does, how to use it, how to read its output, and concrete examples for common data types (strings, integers/floats, arrays, objects, null/boolean). You’ll also get tips for readable output and safe usage.

What var_dump() does

var_dump() prints a human-readable representation of one or more variables to the output. It shows:

  • the type of the variable (e.g., string, int, float, bool, array, object, resource, NULL),

  • the value of the variable,

  • for strings: the length (number of bytes/characters),

  • for arrays/objects: the structure (number of elements/properties and nested contents),

  • for resources: the resource type and id.

var_dump() is intended for debugging and inspection. It writes directly to output (echo-like) and does not return the dumped text (it returns void).

Syntax and basic usage

Basic usage:

var_dump($variable);

You can pass multiple variables:

var_dump($a, $b, $c);

Typical patterns:

  • Quick single-variable inspection:

    var_dump($user);
  • With context for clarity:

    echo '<pre>After login, $user = ';
    var_dump($user);
    echo '</pre>';

Important behavior notes:

  • var_dump() prints directly. It does not return a string you can assign (use print_r($var, true) or var_export($var, true) to get a string).

  • The exact textual formatting is stable but may vary slightly between PHP versions and between CLI and web SAPI (CLI may show line breaks without HTML).

  • Because it outputs directly, use output buffering (ob_start() / ob_get_clean()) if you need to capture the dump.

Example capturing output:

ob_start();
var_dump($data);
$dump = ob_get_clean();
// now $dump contains the textual dump and can be logged
error_log($dump);

Output format explained (type, length, structure)

Here’s how to read the typical pieces in var_dump() output:

  • Scalars

    • int(123) — integer with value 123

    • float(3.14) — floating-point number

    • bool(true) or bool(false) — booleans

    • NULL — null value

    • string(5) "apple" — string of length 5 with value "apple"

  • Arrays

    • array(3) { [...] } — array with 3 elements. Inside you’ll see each key and its dumped value:

      array(2) {
      ["name"]=>
      string(5) "Alice"
      [0]=>
      int(42)
      }
    • Keys shown as ["key"]=> for associative and [0]=> for numeric indices.

  • Objects

    • object(stdClass)#1 (2) { ... } — an stdClass object; #1 is the internal object id, (2) is the number of properties.

    • Inside you’ll see property names and values. Visibility is shown for non-public properties in newer PHP versions (e.g., private/protected markers encoded in the property name representation).

  • Resources

    • resource(5) of type (stream) — resource id and type.

  • Nesting

    • Nested arrays/objects are expanded recursively with indentation to show structure and depth. Circular references are detected and displayed as something like *RECURSION* (so var_dump won’t hang infinitely).

Examples with different data types

Below are concrete examples and the typical var_dump() output you’ll see. (Output formatting shown as you would see it in HTML when wrapped in <pre> or on CLI for readability.)

Strings

$name = "hello";
var_dump($name);

Output:

string(5) "hello"

Explanation: string(5) => 5 characters; then the quoted value.

Integers and floats

$int = 42;
$float = 3.14;
var_dump($int, $float);

Output:

int(42)
float(3.14)

Explanation: shows type and exact stored value.

Booleans and NULL

$true = true;
$false = false;
$nothing = null;
var_dump($true, $false, $nothing);

Output:

bool(true)
bool(false)
NULL

Note: NULL is printed in uppercase.

Arrays (including nested)

$arr = [
"name" => "Alice",
"age" => 30,
"tags" => ["php", "debug"],
0 => 100
];
var_dump($arr);

Output:

array(4) {
["name"]=>
string(5) "Alice"
["age"]=>
int(30)
["tags"]=>
array(2) {
[0]=>
string(3) "php"
[1]=>
string(5) "debug"
}
[0]=>
int(100)
}

Tips reading this:

  • array(4) means 4 elements at the top level.

  • Keys and values follow with indentation; nested arrays are recursively displayed.

Objects

class User {
public $name;
private $secret;
public function __construct($n, $s) {
$this->name = $n;
$this->secret = $s;
}
}

$user = new User(“Bob”, “topsecret”);
var_dump($user);

Possible output (visibility notation may vary by PHP version):

object(User)#1 (2) {
["name"]=>
string(3) "Bob"
["\0User\0secret"]=>
string(9) "topsecret"
}

Explanation:

  • object(User)#1 (2) — an instance of User, internal id 1, with 2 properties.

  • Private/protected properties are shown with special encoded names (e.g. \0Class\0property) so you can tell visibility and origin. Newer PHP versions may display private/protected labels more clearly.

Null & Boolean values (repeated briefly)

Already shown above — bool(true)/bool(false) and NULL.

Advanced examples & behaviors

Circular references

$a = [];
$a['self'] = &$a;
var_dump($a);

Output includes recursion marker:

array(1) {
["self"]=>
&array(1) {
["self"]=>
*RECURSION*
}
}

var_dump() detects circular references and prints *RECURSION* rather than recursing forever.

Resources

$fp = fopen(__FILE__, 'r');
var_dump($fp);
fclose($fp);

Output:

resource(5) of type (stream)

Multiple variables

var_dump($x, $y, $z);

var_dump will output each variable in order.

Practical tips for using var_dump() effectively

  • Wrap in <pre> on web pages for readable formatting:

    echo '<pre>';
    var_dump($var);
    echo '</pre>';
  • Add context (file/line/what you’re dumping):

    echo '<pre>[After fetchUsers] $users:';
    var_dump($users);
    echo '</pre>';
  • Capture output for logging with output buffering:

    ob_start();
    var_dump($data);
    $text = ob_get_clean();
    error_log($text);
  • Don’t leave dumps in production — they can leak sensitive data and disrupt output (especially JSON or HTML responses).

  • Use print_r($var, true) or var_export($var, true) when you need a returned string rather than direct printing.

  • For very large structures, consider limiting the scope of what you dump or using specialized tools (Xdebug, profilers) — huge dumps are hard to read and slow.

  • Prefer var_dump() for type clarity, because it shows types and string lengths (useful for subtle bugs like string vs int).

When var_dump() is not enough

  • For step-through debugging or to inspect call stacks/line-by-line execution, use an interactive debugger (Xdebug + IDE).

  • For performance profiling, use profilers (Xdebug profiler, Blackfire).

  • For structured logging in production, use a logger (Monolog) and avoid raw dumps to users.

 

Understanding print_r()

This section explains PHP’s print_r() in detail: what it does, how to call it, how it differs from var_dump(), when it’s useful, and how to capture its output instead of printing directly.

What print_r() does

print_r() prints a human-readable representation of a variable — most commonly arrays and objects. Its goal is readability: it shows the structure (keys and values, or object properties) in a compact, easy-to-scan format. Unlike var_dump(), it does not show explicit types (e.g., int(5) or string(3)) or string lengths; it focuses on the content and structure rather than type details.

Typical uses:

  • Quickly examining an array’s keys and values.

  • Showing object properties in a readable form.

  • Producing a concise, readable text representation suitable for logs.

Syntax and basic usage

Basic call:

print_r($variable);

You can send the output to a string by passing true as the second argument:

$string = print_r($variable, true);

Examples:

$array = ['name' => 'Alice', 'age' => 30];
print_r($array);

/*
Output:
Array
(
[name] => Alice
[age] => 30
)
*/

For objects:

class User { public $name; public $age; }
$user = new User();
$user->name = 'Bob';
$user->age = 25;
print_r($user);

/*
Output:
User Object
(
[name] => Bob
[age] => 25
)
*/

Notes:

  • When used without the second parameter or with the second parameter set to false, print_r() prints directly to output and returns true on success.

  • When the second parameter is true, it returns the output as a string (and does not print).

Differences between print_r() and var_dump()

Key distinctions:

  • Detail vs readability

    • var_dump() shows type information and string lengths (string(5) "hello"), and has verbose output suitable when you need type precision.

    • print_r() shows a cleaner structure without explicit type labels — easier to read at a glance for arrays/objects.

  • Return behavior

    • var_dump() prints directly and returns void (so you must capture its output with output buffering to get a string).

    • print_r() can return a string if called with the second argument true ($s = print_r($var, true);).

  • Output format

    • var_dump() uses an indented, type-annotated format.

    • print_r() uses a more compact “Array (…)” or “ClassName Object (…)” format that’s often used in logs.

  • Use cases

    • Use var_dump() when you care about types or need to distinguish string vs int vs float.

    • Use print_r() when you want a quick readable snapshot of structure or to include a clean textual representation in logs.

  • var_export() comparison

    • var_export() returns a valid PHP code representation of the value (useful if you want to recreate the value by eval() or write it into a PHP file). print_r() does not guarantee valid PHP code.

Using print_r() for quick inspection

print_r() is ideal for fast, human-friendly checks:

  • Wrap in <pre> in HTML to preserve formatting:

    echo '<pre>';
    print_r($data);
    echo '</pre>';
  • Add context so you know where the dump came from:

    echo '<pre>[After DB fetch] $rows=' . PHP_EOL;
    print_r($rows);
    echo '</pre>';
  • Log instead of printing:

    error_log(print_r($rows, true)); // safe to write to logs
  • Use it in CLI scripts and unit-test debugging for concise output.

Practical tips:

  • For very large arrays/objects, consider printing only a slice or selected keys to avoid unreadable output.

  • Combine with array_slice(), array_keys(), or object property selection to narrow scope before printing.

Returning output instead of printing

To capture print_r() output as a string (so you can log it, manipulate it, or send it elsewhere), pass true as the second parameter:

$text = print_r($variable, true); // $text now contains the formatted representation
// e.g.
file_put_contents('/tmp/debug.txt', $text);

This is often more convenient and safer than printing directly (especially in production code):

  • Use with error_log():

    error_log("DEBUG: " . print_r($data, true));
  • Use with custom loggers:

    $logger->debug(print_r($data, true));
  • Compare with var_dump() capture (if you need var_dump output): use output buffering:

    ob_start();
    var_dump($data);
    $dump = ob_get_clean();

Edge cases and behaviors

  • Objects: print_r() prints object class name and public/protected/private properties; however, it may not display visibility or type metadata as clearly as var_dump() does. Exact representation can vary by PHP version.

  • Circular references: For nested structures with recursion, print_r() will indicate recursion rather than enter an infinite loop (PHP prints *RECURSION* in such cases).

  • Resources: print_r() will show a resource in a reasonable way, but var_dump() is usually better if you need resource type and id.

  • Return values: Remember that without the second argument true, print_r() prints and returns true on success — don’t assume it returns the string.

When to use print_r() (best practice)

Use print_r() when you want:

  • A fast, readable snapshot of arrays or objects.

  • To generate a simple text representation for logs or email.

  • To debug code during development where type precision is not critical.

Prefer other tools when:

  • You need type information (use var_dump()).

  • You require valid PHP code representation (use var_export()).

  • You are debugging complex execution flow and need step-through debugging (use Xdebug/IDE).

Always remember:

  • Remove or guard debug output before shipping to production.

  • Avoid printing sensitive data (passwords, tokens, personal info).

  • For production debugging, prefer structured logs (JSON) and proper log management.

Short examples (cheat-sheet)

// Print directly (readable)
print_r($arr);

// Capture into string (for logging)
$txt = print_r($arr, true);
error_log($txt);

// Compare with var_dump
echo ‘<pre>’;
print_r($arr); // readable, no types
var_dump($arr); // verbose, shows types & lengths
echo ‘</pre>’;

Comparing var_dump() vs print_r()

This section gives a clear, practical comparison so you can choose the right tool fast. It covers when to use each, how readability and detail trade off, performance considerations, and several practical examples (including how to capture output for logs).

When to use each function

Use var_dump() when:

  • You need precise type information (e.g., to see whether a value is int, string, float, bool).

  • You must see string lengths (helps spot hidden whitespace or multibyte issues).

  • You’re inspecting resources and want the resource type/id.

  • You are debugging tricky type/coercion bugs (e.g., "5" vs 5).

Use print_r() when:

  • You want a clean, human-friendly snapshot of arrays or objects (structure and values) without clutter.

  • You are generating simple logs and prefer a compact readable form.

  • You need a string result directly (print_r($var, true)), e.g., to append to a log file or include in an email.

Use other functions when appropriate:

  • var_export($var, true) — if you need a valid PHP code representation of a variable.

  • A proper debugger (Xdebug + IDE) — for step-through debugging, breakpoints, or call stacks.

Readability vs. detail

Readability (print_r() wins):

  • print_r() shows values and keys/property names in a compact, indented format:

    Array
    (
    [name] => Alice
    [age] => 30
    )
  • Good for quick manual inspection at a glance.

Detail (var_dump() wins):

  • var_dump() shows type information and string lengths:

    string(5) "Alice"
    int(30)
  • This helps detect subtle bugs (e.g., empty string "" vs NULL, "0" vs 0, accidental floats vs ints).

Practical takeaway: if you only need the shape of the data and readability, use print_r(). If you suspect a type-related bug or need exact internal representation — use var_dump().

Performance considerations

  • Micro-level: var_dump() does slightly more work (type annotations, string length calculation, more verbose formatting) so it is marginally slower than print_r(). In most development scenarios this is irrelevant.

  • Large structures: Both functions become expensive when dumping very large arrays/objects. The cost is dominated by traversing memory and formatting many items, not the small difference between the two functions.

  • Production impact: Never leave large dumps in production output:

    • They can slow responses, fill logs, and leak sensitive data.

    • If you must log in production, capture and sanitize, then write to structured logs (e.g., JSON) or use print_r($var, true) to get a string and limit/trim it.

  • Capture overhead: Capturing var_dump() output requires output buffering (ob_start() / ob_get_clean()), which adds a little overhead. print_r($var, true) returns a string directly and is cheaper to capture.

Rule of thumb: performance differences are negligible for short debugging runs; for large datasets or production use, avoid dumping entire structures and prefer selective logging or profiling tools.

Practical examples

Below are side-by-side examples showing common situations and the outputs you’d get. Wrap output in <pre> on web pages for readability.

1) Scalar values

$int = 5;
$str = "5";

echo ‘<pre>’;
print_r($int);
print_r($str);
var_dump($int);
var_dump($str);
echo ‘</pre>’;

Outputs (conceptual):

  • print_r($int)5

  • print_r($str)5

  • var_dump($int)int(5)

  • var_dump($str)string(1) "5"

Why it matters: print_r() doesn’t reveal that one is an integer and the other is a string, var_dump() does.

2) Arrays and nested arrays

$arr = ['name' => 'Alice', 'scores' => [10, 20]];
echo '<pre>';
print_r($arr);
var_dump($arr);
echo '</pre>';
  • print_r($arr) is concise and very readable:

    Array
    (
    [name] => Alice
    [scores] => Array
    (
    [0] => 10
    [1] => 20
    )
    )
  • var_dump($arr) includes types and lengths:

    array(2) {
    ["name"]=>
    string(5) "Alice"
    ["scores"]=>
    array(2) {
    [0]=>
    int(10)
    [1]=>
    int(20)
    }
    }

Use print_r() for quick structure checks; var_dump() if you need to know that "10" (string) vs 10 (int) is present.

3) Objects (visibility & private properties)

class User {
public $name;
private $secret;
public function __construct($n, $s) {
$this->name = $n;
$this->secret = $s;
}
}
$u = new User('Bob', 'topsecret');

echo ‘<pre>’;
print_r($u);
var_dump($u);
echo ‘</pre>’;

  • print_r($u) shows class and public properties:

    User Object
    (
    [name] => Bob
    [secret:User:private] => topsecret // representation varies by PHP version
    )
  • var_dump($u) shows object, property count and encoded visibility info:

    object(User)#1 (2) {
    ["name"]=>
    string(3) "Bob"
    ["\0User\0secret"]=>
    string(9) "topsecret"
    }

Note: var_dump() makes visibility and property encoding more explicit; helpful when you need to confirm private/protected state.

4) Capturing output for logs

print_r() (direct):

$txt = print_r($arr, true); // returns string
error_log("DEBUG: " . $txt);

var_dump() (capture):

ob_start();
var_dump($arr);
$dump = ob_get_clean();
error_log("DUMP: " . $dump);

Advice: use print_r($var, true) when you want a quick string for logs; use buffered var_dump() if you need the extra type detail.

5) Circular references

Both handle recursion safely:

$a = [];
$a['self'] = &$a;

echo ‘<pre>’;
print_r($a);
var_dump($a);
echo ‘</pre>’;

Both will indicate recursion (e.g., *RECURSION*), preventing infinite loops. Output formats differ but both protect you.

Quick decision checklist

  • Do you need type and length? → var_dump()

  • Do you want a neat, compact snapshot for logs or quick viewing? → print_r()

  • Do you want a returned string for immediate logging? → print_r($var, true)

  • Do you need valid PHP code representation? → var_export($var, true)

  • Is this production code? → Don’t print raw dumps; sanitize and log selectively.

Final recommendations / best practices

  1. Prefer print_r() for readable structure dumps and when you need a returned string.

  2. Use var_dump() when debugging type issues — it’s more explicit.

  3. Wrap in <pre> for HTML output to keep whitespace and indentation intact.

  4. Capture output safely (print_r($var, true) or ob_start() + var_dump()) before writing to logs.

  5. Never leave raw dumps in production responses; they can leak sensitive data and impair UX. Use structured logs (Monolog, JSON) and add rate/size limits.

  6. When in doubt, start with print_r() for readability, then switch to var_dump() if type details look suspicious.

 

Formatting Output for Easier Debugging

Good formatting makes debugging faster and less painful. This section covers practical techniques for presenting the output of var_dump() / print_r() (and similar helpers) so it’s easy to read in both HTML and CLI contexts, how to control indentation and spacing, and how to combine debug output with helpful, contextual messages.

1) Use <pre> (or equivalent) for readable HTML output

When you dump variables inside a web page, browsers collapse whitespace. Wrap debug output in a <pre> block so the indenting and newlines remain intact:

echo '<pre>';
var_dump($user);
echo '</pre>';

Why <pre> helps:

  • Preserves indentation and line breaks.

  • Makes nested arrays/objects readable at a glance.

  • Works with both var_dump() and print_r().

If you want a tiny bit of styling for readability, place the output inside a container:

echo '<div class="debug"><pre>';
var_dump($data);
echo '</pre></div>';

And optional CSS (put in your dev stylesheet, never inline in production responses with sensitive data):

.debug pre {
background: #f7f7f7;
border: 1px solid #ddd;
padding: 10px;
overflow: auto;
font-family: monospace;
font-size: 13px;
line-height: 1.4;
white-space: pre-wrap; /* wrap long lines */
word-break: break-word;
}

Tip: Use white-space: pre-wrap if some lines are extremely long (like big JSON strings) so the block will wrap rather than forcing horizontal scroll.

2) CLI vs Web — pick the right format

  • CLI scripts: raw var_dump() / print_r() output is fine; no <pre> needed.

  • Web pages / JSON APIs: never simply var_dump() into a JSON response or HTML page returned to users. Either capture and log the output, or restrict dumps to admin/dev-only pages.

Example (capture and log instead of echoing to user):

$dump = print_r($data, true); // safer: get a string
error_log("[DEBUG] " . $dump); // write to server logs instead of user output

3) Add contextual messages (what, where, when)

Always include short labels and context so later you know why that dump was added and where it came from:

echo '<pre>';
echo "[After login] \$user (line " . __LINE__ . " in " . __FILE__ . "):\n";
var_dump($user);
echo '</pre>';

Better: include a timestamp and file/line using debug_backtrace() for heavy debugging:

function debug_context() {
$bt = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 1)[0];
return sprintf("[%s] %s:%d", date('Y-m-d H:i:s'), $bt['file'], $bt['line']);
}

echo ‘<pre>’ . debug_context() . “\n”;
var_dump($data);
echo ‘</pre>’;

This is invaluable when you have many dumps across files or repeated requests.

4) Indentation and spacing tips

var_dump() and print_r() produce their own indentation. You can improve readability further by:

  • Separating dumps with blank lines and labels:

    echo '<pre>';
    echo "=== Start of debug ===\n\n";
    var_dump($a);
    echo "\n\n--- Next dump ---\n\n";
    print_r($b);
    echo "\n=== End ===\n";
    echo '</pre>';
  • Printing one variable per block (avoid cramming many different variables together — makes scanning harder).

  • Slicing large arrays before dumping when you only need the head/tail:

    $head = array_slice($bigArray, 0, 20, true);
    echo '<pre>';
    print_r($head);
    echo '</pre>';
  • Showing only keys or counts to assess structure quickly:

    echo 'count: ' . count($arr) . "\n";
    echo 'keys: ' . implode(', ', array_keys($arr)) . "\n";

5) Combining debug functions with custom messages

You can combine echo / printf / error_log with var_dump()/print_r() to make output actionable:

  • Human-readable note + dump

    echo '<pre>';
    printf("Checkpoint: Processed %d users; first user:\n", count($users));
    print_r($users[0]);
    echo '</pre>';
  • Conditional debugging (only show in dev mode)

    if (defined('APP_ENV') && APP_ENV === 'development') {
    echo '<pre>';
    var_dump($data);
    echo '</pre>';
    }
  • Log with context instead of showing to user

    $msg = '[' . date('c') . "] [ORDER_DEBUG] " . print_r($order, true);
    error_log($msg);
  • Combine JSON formatting for readability
    If the structure is JSON-serializable, pretty-print it:

    echo '<pre>';
    echo json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
    echo '</pre>';

    This gives a compact, consistent indentation for arrays/objects and is excellent for APIs or when you want machine- and human-readable output.

6) Capture, sanitize, and limit output before logging

Large dumps or dumps that include secrets are dangerous. Always consider:

  • Capture instead of echoing:

    ob_start();
    var_dump($data);
    $dump = ob_get_clean();
  • Sanitize sensitive fields before logging (mask passwords, tokens, PII):

    if (isset($user['password'])) {
    $user['password'] = '***REDACTED***';
    }
    error_log(print_r($user, true));
  • Trim very large output:

    $text = print_r($bigData, true);
    if (strlen($text) > 10000) {
    $text = substr($text, 0, 10000) . "\n...TRUNCATED...\n";
    }
    error_log($text);
  • Use structured logs (JSON) when possible so log aggregators can parse and search dumps easily:

    $logEntry = [
    'time' => date('c'),
    'event' => 'debug',
    'payload' => $data // but sanitize first
    ];
    error_log(json_encode($logEntry, JSON_UNESCAPED_UNICODE));

7) Make a small reusable debug helper

A tiny helper centralizes formatting, context, safe-capture, and dev-only gating:

function ddump($var, $label = null, $toLog = false) {
$label = $label ? $label . "\n" : '';
$context = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 1)[0];
$header = sprintf("[%s] %s:%d\n", date('Y-m-d H:i:s'), $context['file'], $context['line']);

// Capture var_dump output to string
ob_start();
var_dump($var);
$body = ob_get_clean();

$text = $header . $label . $body;

if ($toLog) {
error_log($text);
return;
}

// Output safely for HTML pages
echo ‘<pre>’ . htmlspecialchars($text, ENT_QUOTES | ENT_SUBSTITUTE, ‘UTF-8’) . ‘</pre>’;
}

Usage:

ddump($user, 'After login', false); // prints to browser (dev only)
ddump($order, 'Order payload', true); // logs to error log

This keeps consistent formatting and avoids ad-hoc prints scattered across the code.

8) Additional best practices & warnings

  • Do not leave debug output in production — it can leak internal state, secrets, and break API/HTML structure.

  • Use feature flags or environment checks so debug output is shown only in development.

  • Prefer logging to server-side logs rather than echoing into responses that reach end users.

  • Limit the size of any dumped structure to something reasonable; use sampling or partial dumps for huge datasets.

  • Avoid dumping binary data or files — they will ruin display and logs; instead log metadata (size, mime type).

  • When working with frameworks, consider their built-in debug tools (e.g., debug panels, loggers) or dedicated debugging libraries — but helper functions above are still useful for small scripts.

 

 

Debugging Arrays and Multidimensional Structures

Arrays are where many PHP bugs hide: wrong keys, unexpected nesting, mixed types, empty nodes, or circular references. This section gives practical, hands-on techniques for working with nested arrays, debugging associative arrays, and reading complex output so you can find the problem fast.

1 — First principles: what to inspect

When you inspect an array, check these things in order:

  1. Existence — Does the variable exist and is it an array?

    if (!isset($arr) || !is_array($arr)) var_dump($arr);
  2. Size — How many top-level elements?

    echo 'count: ' . count($arr);
  3. Keys — Are keys what you expect (numeric vs string)?

    print_r(array_keys($arr));
  4. Value types — Are values the types you expect? (string vs int vs array vs null)
    Use var_dump() for types.

  5. Depth & shape — How deeply nested is it and are nested structures consistent?

  6. Edge cases — Empty arrays, null values, unexpected keys, or circular references.

Start broad (existence/size), then drill into suspicious parts.

2 — Working with nested arrays (practical patterns)

A. Inspect only the head (avoid huge dumps)

For very large arrays, dump just the start so you can see shape without overwhelming output:

$head = array_slice($nested, 0, 10, true);
echo '<pre>';
print_r($head);
echo '</pre>';

B. Show keys + depth summary

Quick function to show counts per depth (simple breadth-first):

function depth_summary(array $arr, $maxDepth = 3) {
$summary = [];
$queue = [[$arr, 0]];
while ($queue) {
list($a, $d) = array_shift($queue);
if ($d > $maxDepth) continue;
$summary[$d] = ($summary[$d] ?? 0) + count($a);
foreach ($a as $v) {
if (is_array($v)) $queue[] = [$v, $d + 1];
}
}
return $summary;
}
print_r(depth_summary($nested));

This quickly tells you if nesting is deeper than expected.

C. Use array_walk_recursive() to examine leaves

If you care about leaf values only:

$leaves = [];
array_walk_recursive($nested, function($v, $k) use (&$leaves) {
$leaves[] = [$k, $v, gettype($v)];
});
print_r(array_slice($leaves, 0, 20));

D. Search for a key or value deeply

Find where a particular id/key/value appears:

function find_paths(array $arr, $target) {
$paths = [];
$stack = [['path' => [], 'node' => $arr]];
while ($stack) {
$frame = array_pop($stack);
foreach ($frame['node'] as $k => $v) {
$path = array_merge($frame['path'], [$k]);
if ($v === $target) $paths[] = $path;
if (is_array($v)) $stack[] = ['path' => $path, 'node' => $v];
}
}
return $paths;
}
print_r(find_paths($nested, 'expected_value'));

E. Pretty-print JSON for consistent visual shape

If the structure is JSON-serializable, json_encode(..., JSON_PRETTY_PRINT) gives compact, consistent indentation:

echo '<pre>' . json_encode($nested, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE) . '</pre>';

This can be easier to scan than var_dump() for large homogeneous structures.

3 — Debugging associative arrays

Associative arrays often fail because keys are misspelled, missing, or of the wrong type (e.g., '123' vs 123).

A. List keys and compare to expected set

$actual = array_keys($assoc);
$expected = ['id','name','email','status'];
$missing = array_diff($expected, $actual);
$extra = array_diff($actual, $expected);
print_r(['missing' => $missing, 'extra' => $extra]);

B. Check types of values for critical keys

foreach (['id','email','active'] as $k) {
printf("%s: %s\n", $k, isset($assoc[$k]) ? gettype($assoc[$k]) : 'MISSING');
}

C. Defensive access pattern

When you suspect missing keys, don’t assume direct indexing. Use ?? or array_key_exists():

$name = $assoc['name'] ?? '<<missing name>>';

D. Detect accidental numeric-string keys

Sometimes numeric indices are strings; check for numeric-string keys:

foreach (array_keys($assoc) as $k) {
if (is_string($k) && ctype_digit($k)) {
echo "Numeric-string key found: $k\n";
}
}

4 — Strategies for reading complex output

1. Add human context

Always label dumps so you know where they came from:

echo '<pre>[After fetchOrders()] ' . date('c') . "\n";
print_r($orders);
echo '</pre>';

2. One dump per output block

Dump one array at a time with a title — scanning is easier than scanning a single massive dump with everything.

3. Show only keys/shape first, then values

First print_r(array_keys(...)) or array_map('gettype', ...) so you understand structure, then inspect values for suspicious keys.

4. Use var_dump() when types matter

If you suspect '0' vs 0 or empty string vs null, var_dump() is the right tool because it shows types and string lengths.

5. Truncate long strings and large subtrees

Large blobs (long strings, very big arrays) hide problems. Truncate:

$txt = print_r($bigString, true);
echo substr($txt, 0, 200) . "\n...TRUNCATED...\n";

For arrays, inspect representative elements only (array_slice).

6. Detect circular references

If var_dump() shows *RECURSION*, you have circular references. Handle with care:

  • Break recursion when serializing (write a custom walker that records object ids).

  • For debugging, replace nested references with a placeholder after detecting spl_object_id() or reference id.

Example: avoid infinite recursion with object tracking:

function safe_dump($var, &$seen = []) {
if (is_object($var)) {
$id = spl_object_id($var);
if (isset($seen[$id])) return '*RECURSION*';
$seen[$id] = true;
// inspect object properties...
}
if (is_array($var)) {
$out = [];
foreach ($var as $k=>$v) $out[$k] = safe_dump($v, $seen);
return $out;
}
return $var;
}
print_r(safe_dump($complex));

7. Use small helper functions to standardize dumps

A reusable helper centralizes safety, truncation, and context. Example:

function debug_array(array $arr, $label = null, $maxItems = 50) {
$label = $label ? "[$label]\n" : '';
$summary = sprintf("count=%d\n", count($arr));
$slice = array_slice($arr, 0, $maxItems, true);
echo '<pre>' . $label . $summary . print_r($slice, true);
if (count($arr) > $maxItems) echo "\n...TRUNCATED (showing first $maxItems) ...";
echo '</pre>';
}

5 — Tools & advanced approaches

  • Convert to JSON for quick scanning (JSON_PRETTY_PRINT) — best for arrays with scalar values.

  • Iterators and Generators — if the array is produced lazily, debug the generator steps rather than materializing huge arrays.

  • Xdebug/IDE tools — use breakpoints to inspect variables interactively (especially useful for large, nested structures).

  • VarDumper (Symfony) — richer output with colors and depth control if you have it available.

  • Log structured data (JSON) to log files and use log viewers/grep instead of dumping to HTML.

6 — Common pitfalls and how to avoid them

  • Dumping entire huge arrays — slows execution, bloats logs. Inspect slices or use summaries.

  • Leaving debug output in production — leaks PII or breaks responses. Gate debug output with environment checks.

  • Misreading associative vs numeric keys — always inspect array_keys() when indexing fails.

  • Ignoring types — use var_dump() when behavior depends on type (e.g., in_array() strict vs non-strict).

  • Missing circular references — detect with *RECURSION* and avoid naive json_encode() on such structures.

7 — Quick checklist to debug any array problem

  1. isset() / is_array() — confirm variable presence and type.

  2. count() and array_keys() — check size and keys.

  3. print_r(array_slice(...)) — inspect first N items.

  4. var_dump() on suspicious items — validate types and string lengths.

  5. array_walk_recursive() or custom walker — examine leaves or search for values.

  6. Capture with print_r($var, true) or buffer var_dump() before logging, sanitize, and truncate.

  7. Remove or guard debug statements before deploying.

Examples (short end-to-end)

  1. Find missing key in list of user arrays

foreach ($users as $i => $u) {
if (!array_key_exists('email', $u)) {
echo "Missing email at index $i\n";
print_r($u); // small dump to inspect
break;
}
}
  1. Find object with unexpected property type

array_walk_recursive($data, function($v,$k) {
if ($k === 'id' && !is_int($v)) {
var_dump($v);
}
});

 

Debugging Arrays and Multidimensional Structures

Arrays are where many PHP bugs hide: wrong keys, unexpected nesting, mixed types, empty nodes, or circular references. This section gives practical, hands-on techniques for working with nested arrays, debugging associative arrays, and reading complex output so you can find the problem fast.

1 — First principles: what to inspect

When you inspect an array, check these things in order:

  1. Existence — Does the variable exist and is it an array?

    if (!isset($arr) || !is_array($arr)) var_dump($arr);
  2. Size — How many top-level elements?

    echo 'count: ' . count($arr);
  3. Keys — Are keys what you expect (numeric vs string)?

    print_r(array_keys($arr));
  4. Value types — Are values the types you expect? (string vs int vs array vs null)
    Use var_dump() for types.

  5. Depth & shape — How deeply nested is it and are nested structures consistent?

  6. Edge cases — Empty arrays, null values, unexpected keys, or circular references.

Start broad (existence/size), then drill into suspicious parts.

2 — Working with nested arrays (practical patterns)

A. Inspect only the head (avoid huge dumps)

For very large arrays, dump just the start so you can see shape without overwhelming output:

$head = array_slice($nested, 0, 10, true);
echo '<pre>';
print_r($head);
echo '</pre>';

B. Show keys + depth summary

Quick function to show counts per depth (simple breadth-first):

function depth_summary(array $arr, $maxDepth = 3) {
$summary = [];
$queue = [[$arr, 0]];
while ($queue) {
list($a, $d) = array_shift($queue);
if ($d > $maxDepth) continue;
$summary[$d] = ($summary[$d] ?? 0) + count($a);
foreach ($a as $v) {
if (is_array($v)) $queue[] = [$v, $d + 1];
}
}
return $summary;
}
print_r(depth_summary($nested));

This quickly tells you if nesting is deeper than expected.

C. Use array_walk_recursive() to examine leaves

If you care about leaf values only:

$leaves = [];
array_walk_recursive($nested, function($v, $k) use (&$leaves) {
$leaves[] = [$k, $v, gettype($v)];
});
print_r(array_slice($leaves, 0, 20));

D. Search for a key or value deeply

Find where a particular id/key/value appears:

function find_paths(array $arr, $target) {
$paths = [];
$stack = [['path' => [], 'node' => $arr]];
while ($stack) {
$frame = array_pop($stack);
foreach ($frame['node'] as $k => $v) {
$path = array_merge($frame['path'], [$k]);
if ($v === $target) $paths[] = $path;
if (is_array($v)) $stack[] = ['path' => $path, 'node' => $v];
}
}
return $paths;
}
print_r(find_paths($nested, 'expected_value'));

E. Pretty-print JSON for consistent visual shape

If the structure is JSON-serializable, json_encode(..., JSON_PRETTY_PRINT) gives compact, consistent indentation:

echo '<pre>' . json_encode($nested, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE) . '</pre>';

This can be easier to scan than var_dump() for large homogeneous structures.

3 — Debugging associative arrays

Associative arrays often fail because keys are misspelled, missing, or of the wrong type (e.g., '123' vs 123).

A. List keys and compare to expected set

$actual = array_keys($assoc);
$expected = ['id','name','email','status'];
$missing = array_diff($expected, $actual);
$extra = array_diff($actual, $expected);
print_r(['missing' => $missing, 'extra' => $extra]);

B. Check types of values for critical keys

foreach (['id','email','active'] as $k) {
printf("%s: %s\n", $k, isset($assoc[$k]) ? gettype($assoc[$k]) : 'MISSING');
}

C. Defensive access pattern

When you suspect missing keys, don’t assume direct indexing. Use ?? or array_key_exists():

$name = $assoc['name'] ?? '<<missing name>>';

D. Detect accidental numeric-string keys

Sometimes numeric indices are strings; check for numeric-string keys:

foreach (array_keys($assoc) as $k) {
if (is_string($k) && ctype_digit($k)) {
echo "Numeric-string key found: $k\n";
}
}

4 — Strategies for reading complex output

1. Add human context

Always label dumps so you know where they came from:

echo '<pre>[After fetchOrders()] ' . date('c') . "\n";
print_r($orders);
echo '</pre>';

2. One dump per output block

Dump one array at a time with a title — scanning is easier than scanning a single massive dump with everything.

3. Show only keys/shape first, then values

First print_r(array_keys(...)) or array_map('gettype', ...) so you understand structure, then inspect values for suspicious keys.

4. Use var_dump() when types matter

If you suspect '0' vs 0 or empty string vs null, var_dump() is the right tool because it shows types and string lengths.

5. Truncate long strings and large subtrees

Large blobs (long strings, very big arrays) hide problems. Truncate:

$txt = print_r($bigString, true);
echo substr($txt, 0, 200) . "\n...TRUNCATED...\n";

For arrays, inspect representative elements only (array_slice).

6. Detect circular references

If var_dump() shows *RECURSION*, you have circular references. Handle with care:

  • Break recursion when serializing (write a custom walker that records object ids).

  • For debugging, replace nested references with a placeholder after detecting spl_object_id() or reference id.

Example: avoid infinite recursion with object tracking:

function safe_dump($var, &$seen = []) {
if (is_object($var)) {
$id = spl_object_id($var);
if (isset($seen[$id])) return '*RECURSION*';
$seen[$id] = true;
// inspect object properties...
}
if (is_array($var)) {
$out = [];
foreach ($var as $k=>$v) $out[$k] = safe_dump($v, $seen);
return $out;
}
return $var;
}
print_r(safe_dump($complex));

7. Use small helper functions to standardize dumps

A reusable helper centralizes safety, truncation, and context. Example:

function debug_array(array $arr, $label = null, $maxItems = 50) {
$label = $label ? "[$label]\n" : '';
$summary = sprintf("count=%d\n", count($arr));
$slice = array_slice($arr, 0, $maxItems, true);
echo '<pre>' . $label . $summary . print_r($slice, true);
if (count($arr) > $maxItems) echo "\n...TRUNCATED (showing first $maxItems) ...";
echo '</pre>';
}

5 — Tools & advanced approaches

  • Convert to JSON for quick scanning (JSON_PRETTY_PRINT) — best for arrays with scalar values.

  • Iterators and Generators — if the array is produced lazily, debug the generator steps rather than materializing huge arrays.

  • Xdebug/IDE tools — use breakpoints to inspect variables interactively (especially useful for large, nested structures).

  • VarDumper (Symfony) — richer output with colors and depth control if you have it available.

  • Log structured data (JSON) to log files and use log viewers/grep instead of dumping to HTML.

6 — Common pitfalls and how to avoid them

  • Dumping entire huge arrays — slows execution, bloats logs. Inspect slices or use summaries.

  • Leaving debug output in production — leaks PII or breaks responses. Gate debug output with environment checks.

  • Misreading associative vs numeric keys — always inspect array_keys() when indexing fails.

  • Ignoring types — use var_dump() when behavior depends on type (e.g., in_array() strict vs non-strict).

  • Missing circular references — detect with *RECURSION* and avoid naive json_encode() on such structures.

7 — Quick checklist to debug any array problem

  1. isset() / is_array() — confirm variable presence and type.

  2. count() and array_keys() — check size and keys.

  3. print_r(array_slice(...)) — inspect first N items.

  4. var_dump() on suspicious items — validate types and string lengths.

  5. array_walk_recursive() or custom walker — examine leaves or search for values.

  6. Capture with print_r($var, true) or buffer var_dump() before logging, sanitize, and truncate.

  7. Remove or guard debug statements before deploying.

Examples (short end-to-end)

  1. Find missing key in list of user arrays

foreach ($users as $i => $u) {
if (!array_key_exists('email', $u)) {
echo "Missing email at index $i\n";
print_r($u); // small dump to inspect
break;
}
}
  1. Find object with unexpected property type

array_walk_recursive($data, function($v,$k) {
if ($k === 'id' && !is_int($v)) {
var_dump($v);
}
});

 

Debugging Objects — full guide

Objects introduce extra complexity vs arrays: properties can be public/private/protected, some data is lazily loaded, objects can contain other objects (nested), and there can be circular references. Below is a practical, hands-on guide to inspect objects safely and effectively using var_dump(), print_r(), casting, reflection, and small helper functions — plus recommendations for production safety.

1) Basic ways to output an object

var_dump() — most detailed

var_dump($obj) prints the object class, an internal id, property count, and each property with its type/value. It is the most precise tool because it exposes types and encoded names for non-public properties.

Example:

class User {
public $name;
private $secret;
protected $role;
public function __construct($n, $s, $r) {
$this->name = $n;
$this->secret = $s;
$this->role = $r;
}
}

$user = new User(“Alice”, “topsecret”, “admin”);
echo ‘<pre>’;
var_dump($user);
echo ‘</pre>’;

Typical var_dump() output (conceptual):

object(User)#1 (3) {
["name"]=>
string(5) "Alice"
["\0User\0secret"]=>
string(9) "topsecret"
["\0*\0role"]=>
string(5) "admin"
}

Notes:

  • Private properties appear with an encoded name "\0ClassName\0property".

  • Protected properties are encoded like "\0*\0property" (or shown with visibility in newer PHP versions).

  • var_dump() shows types (e.g., string(5)), which helps diagnose type issues.

print_r() — more readable

print_r($obj) prints a human-friendly summary. It often shows class name and properties in a compact form. For non-public properties the exact printed notation depends on PHP version; some versions show property:Class:private.

Example:

echo '<pre>';
print_r($user);
echo '</pre>';

Output example:

User Object
(
[name] => Alice
[secret:User:private] => topsecret
[role:protected] => admin
)

print_r() is nice for quick reads, but it doesn’t show explicit types or string lengths.

Casting to array — reveals encoded property names

Casting an object to array shows all properties — including private/protected — but private/protected names are still encoded in array keys:

$arr = (array) $user;
print_r($arr);

Example resulting keys:

Array
(
[name] => Alice
[\0User\0secret] => topsecret
[\0*\0role] => admin
)

This is useful if you want to inspect everything quickly, but be careful: those encoded keys are not the same as public property names.

2) Inspecting only public properties

If you want a simple associative array of public properties only, use get_object_vars():

$public = get_object_vars($user);
print_r($public);
// Array ( [name] => Alice )

get_object_vars() returns only accessible (public) properties in the current scope.

3) Debugging nested objects

Objects often contain other objects (composition). Use recursive techniques but protect against circular references.

Simple nested dump

class Post {
public $author;
public $title;
public function __construct($author, $title) {
$this->author = $author;
$this->title = $title;
}
}

$post = new Post($user, “Hello”);
echo ‘<pre>’;
var_dump($post);
echo ‘</pre>’;

var_dump() will expand nested objects. If there are circular references, PHP protects you by showing *RECURSION* in the output.

Safe recursive dump with object tracking

If you need a customizable dump (e.g., output to JSON, limit depth, avoid repeating the same object), track object ids (spl_object_id()):

function dump_object_safe($var, $maxDepth = 3, $depth = 0, &$seen = []) {
if ($depth > $maxDepth) return '...max depth reached...';
if (is_object($var)) {
$id = spl_object_id($var);
if (isset($seen[$id])) return '*RECURSION*';
$seen[$id] = true;
$arr = ['__class' => get_class($var)];
foreach ((array) $var as $k => $v) {
$arr[$k] = dump_object_safe($v, $maxDepth, $depth + 1, $seen);
}
return $arr;
} elseif (is_array($var)) {
$out = [];
foreach ($var as $k => $v) $out[$k] = dump_object_safe($v, $maxDepth, $depth + 1, $seen);
return $out;
} else {
return $var;
}
}

echo ‘<pre>’;
print_r(dump_object_safe($post));
echo ‘</pre>’;

This returns a structured array you can print_r() or json_encode() safely. Key advantages:

  • Avoids infinite recursion.

  • Lets you limit depth.

  • Preserves class names.

4) Understanding visibility (public / protected / private)

Visibility affects what you can access from where and how debug outputs present properties.

Public

  • Accessible from anywhere.

  • Shown plainly as ["name"]=> in var_dump() and as [name] => in print_r() and (array) casts.

Protected

  • Accessible only within the class and descendants.

  • In var_dump() / (array) it’s encoded as "\0*\0prop"; in print_r() it may be shown as prop:protected.

Private

  • Accessible only inside the declaring class.

  • Encoded in var_dump() / (array) as "\0ClassName\0prop".

  • When a subclass declares a property with the same name, it becomes a different property (different encoded key) because private properties are bound to the declaring class.

Example showing private origin

class A { private $x = 1; }
class B extends A { private $x = 2; }

$b = new B();
print_r((array)$b);

You’ll see two keys with different encodings, e.g.:

Array
(
[\0A\0x] => 1
[\0B\0x] => 2
)

This explains subtle bugs where a subclass and parent both use the same private property name but store separate values.

5) Inspecting private/protected properties with Reflection

If you need to inspect or read non-public properties programmatically, use ReflectionProperty and set it accessible:

$ref = new ReflectionClass($user);
$props = $ref->getProperties();

foreach ($props as $p) {
$p->setAccessible(true); // allow reading private/protected
echo $p->getName() . ‘ => ‘;
var_dump($p->getValue($user));
}

This prints property names and values regardless of visibility. Use responsibly (only for debugging or tooling) — bypassing visibility breaks encapsulation.

6) Customizing debug output in your classes

Classes can provide a nicer debug view.

__debugInfo() (PHP >= 5.6)

If present, var_dump() uses __debugInfo() return value to show the object content:

class User {
private $secret = 'top';
public $name = 'Alice';
public function __debugInfo() {
return ['name' => $this->name, 'secret' => '***REDACTED***'];
}
}
var_dump(new User());

Output will show only name and the sanitized secret substitute. This is ideal to avoid leaking secrets in dumps.

JsonSerializable and json_encode()

Implement JsonSerializable or a toArray() method to control what gets serialized:

class User implements JsonSerializable {
public function jsonSerialize() {
return ['name' => $this->name]; // no secret
}
}

echo json_encode($user, JSON_PRETTY_PRINT);

Use this when you need readable, safe dumps or logging.

7) Capturing object dumps (logging rather than echoing)

var_dump() prints directly. To capture it as string for logs:

ob_start();
var_dump($obj);
$dump = ob_get_clean();
error_log($dump);

For print_r() you can do:

error_log(print_r($obj, true));

Prefer capturing & sanitizing before logging in production (mask passwords, tokens, PII).

8) Dealing with circular references and identity

  • Use spl_object_id() (or spl_object_hash() on older PHP) to identify objects uniquely while traversing.

  • When serializing or converting to arrays, record seen object ids to avoid recursion and to optionally replace repeated objects with a reference marker like {"__ref":id}.

Example marker:

function object_to_array_with_ids($obj, &$seen = []) {
if (is_object($obj)) {
$id = spl_object_id($obj);
if (isset($seen[$id])) return ['__ref' => $id];
$seen[$id] = true;
$arr = ['__class' => get_class($obj), '__id' => $id];
foreach ((array)$obj as $k => $v) $arr[$k] = object_to_array_with_ids($v, $seen);
return $arr;
} elseif (is_array($obj)) {
$out = [];
foreach ($obj as $k => $v) $out[$k] = object_to_array_with_ids($v, $seen);
return $out;
}
return $obj;
}

9) Practical helper: odump() — readable, safe object debug

function odump($var, $label = null, $maxDepth = 3) {
$label = $label ? $label . "\n" : '';
$seen = [];
$data = dump_object_safe($var, $maxDepth, 0, $seen); // from earlier safe function
echo '<pre>' . htmlspecialchars($label . print_r($data, true), ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') . '</pre>';
}

odump():

  • Uses safe traversal,

  • Limits depth,

  • Escapes HTML (safe for web display),

  • Lets you pass a label.

10) Common pitfalls & best practices

  • Don’t rely on (array) cast keys for property names: encoded keys are not the same as public names — use Reflection if you need canonical names.

  • Private property duplication: subclass + parent private properties with same name are different storage locations — beware when debugging unexpected values.

  • Avoid dumping secrets: use __debugInfo(), JsonSerializable, or sanitize before logging.

  • Gate dumps: only display detailed dumps in development environment; write logs instead of returning debug output to API consumers.

  • Prefer structured output for tooling: json_encode() or arrays are easier to parse by log tools; use JSON_PRETTY_PRINT for human readability.

  • Use reflection carefully: it’s powerful for debugging but breaks encapsulation — confine it to debug tools.

Capturing Debug Output

Sometimes you want to save the results of var_dump() or print_r() instead of printing them directly to the screen. This is useful when:

  • You want to log debug output into a file.

  • You need to process or filter the output before displaying it.

  • You cannot print directly to the browser (e.g., debugging AJAX, APIs, CLI scripts).

  • You want to avoid breaking HTML or JSON output.

PHP gives you multiple reliable ways to capture debug output instead of sending it directly to the browser.

1. Storing Function Output in Variables

Some debugging functions can either print output directly or return it as a string depending on how you call them.

print_r($var, true) — return output as string

print_r() has an optional second parameter:

  • false → print to output (default)

  • true → return as a string

Example:

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

$debug = print_r($data, true); // capture output as string

echo “Captured debug: \n” . $debug;

This is helpful when you want to:

  • Save output in logs

  • Send debug info in emails

  • Store debug traces in an array

  • Return structured debug messages in APIs

Important: var_dump() cannot return output by itself — it always prints.
To capture var_dump, you need output buffering (next section).

2. Logging Debug Output to Files

Captured debug output can be written to files using:

A) error_log()

Simple and widely used:

$debug = print_r($data, true);
error_log($debug);

You can also specify a custom debug log file:

error_log($debug, 3, __DIR__ . '/debug.log');

Benefits:

  • Works everywhere (CLI, web, APIs)

  • Does not break HTML or JSON output

  • Can be viewed using standard server logs

  • Safe for production when used properly

B) Using file_put_contents()

Perfect for creating custom debugging logs:

$debug = print_r($data, true);
file_put_contents('debug.txt', $debug . "\n\n", FILE_APPEND);

Use it when you need your own separate debug file.

3. Using ob_start() and ob_get_clean()

(Output Buffering — capturing var_dump, echo, print_r, etc.)

ob_start() and ob_get_clean() allow you to capture ANY output that would normally be printed — including var_dump().

How it works

  1. ob_start() → start capturing output

  2. Run functions that normally print

  3. ob_get_clean() → stop capturing and return the output as a string

Example: Capture var_dump()

ob_start(); // Start capturing output
var_dump($data); // var_dump prints — but we capture it
$dump = ob_get_clean(); // End capture and return output as string

echo “Captured dump:\n” . $dump;

Now you can:

  • Log $dump

  • Email it

  • Store it

  • Return it in an API response

More advanced example: combined debug capture

ob_start();

echo “Before dump:\n”;
var_dump($user);
echo “After dump.”;

$debugOutput = ob_get_clean();

file_put_contents(‘debug.log’, $debugOutput, FILE_APPEND);

Everything inside the buffer is stored, not printed.

When should you use Output Buffering?

Use ob_start() when:

  • You need to capture var_dump(), which otherwise cannot be returned

  • You’re debugging AJAX, JSON APIs, or CLI scripts and printing would break the output

  • You want to sanitize or format debug strings before showing them

  • You want to combine multiple debug outputs into a single structured log entry

 

Common Debugging Mistakes

Even though var_dump() and print_r() are essential debugging tools, many developers use them in inefficient or risky ways. Understanding these common mistakes will help you debug faster, avoid confusion, and prevent accidental errors in production.

1. Forgetting to Remove Debug Statements

This is the most common debugging mistake — and sometimes the most dangerous.

Why it is a problem

  1. Breaks page layouts
    Debug output inside HTML pages creates messy layouts and can break your CSS or JavaScript.

  2. Leaks sensitive data
    var_dump($user) might accidentally reveal:

    • passwords

    • API tokens

    • database results

    • server paths

    • personal user information

  3. Slows down applications
    Dumping large arrays/objects consumes memory and CPU.

  4. Causes unexpected output in APIs
    Extra output breaks responses in:

    • JSON APIs

    • AJAX

    • XML feeds

    • file downloads

How to avoid this mistake

  • Wrap debug code in environment checks:

if ($_ENV['APP_ENV'] === 'dev') {
var_dump($data);
}
  • Use a temporary logger instead of printing.

  • Use // TODO: remove debug comments to remind yourself.

  • Before deploying, search for:

    • var_dump(

    • print_r(

    • echo

    • dd( (Laravel)

    • dump(

2. Debugging Before Isolating the Problem

Another major mistake is dumping everything without understanding what exactly you’re looking for.

Symptoms

  • Adding var_dump() dozens of times in different files

  • Dumping huge complex arrays

  • Checking variables unrelated to the bug

  • Guessing instead of investigating

Why it is a problem

  • You waste time examining irrelevant output.

  • Too much debug output becomes confusing.

  • The actual issue gets buried in noise.

  • You may debug the symptoms, not the cause.

How to fix this mistake

A) Reproduce the bug first

Ensure you can trigger it consistently.

B) Isolate the smallest piece of code causing the issue

Find the exact function, class, or line where the bug appears.

C) Add debug output only around the suspicious area

Example:

// Good: targeted dump
var_dump($order->total);
// Bad: dumping the entire order system
var_dump($allOrders, $user, $cart, $config, $session);

D) Reduce the input

Try smaller datasets so the problem is easier to see.

3. Misinterpreting Output Formats

Developers often misunderstand what var_dump() or print_r() is actually showing.

Common misunderstandings

A) Confusing numeric strings with integers

string(1) "5" // NOT the same as integer(5)

This can break:

  • strict comparisons (===)

  • array keys

  • type-sensitive functions

B) Confusing NULL, false, and empty strings

These three often look similar logically but behave very differently.

var_dump() is the best tool to distinguish them:

NULL
bool(false)
string(0) ""

C) Misreading private/protected object properties

var_dump() shows them with encoded names:

["\0User\0password"] => string(8) "secret"
["\0*\0role"] => string(5) "admin"

Many developers mistake these encoded names for bugs, but this is normal PHP behavior.

D) Confusing recursion markers

When PHP shows:

*RECURSION*

It does not mean your data is invalid.
It means you have circular references — e.g. object A contains object B, and B contains A.

E) Misreading arrays produced by (array)$object

Casting an object to an array shows all properties, including private/protected ones with encoded names.
Developers often think this is a broken array — it’s simply PHP exposing property visibility.

How to avoid misinterpreting output

  • Prefer var_dump() when you want to understand types.

  • Use <pre> tags in HTML for readability.

  • Check PHP documentation for output formats.

  • Use helper functions to normalize objects/arrays before inspecting.

  • Use tools like Symfony VarDumper or Kint which provide color-coded output.

 

 

 

 

Best Practices for Debugging in PHP

Effective debugging in PHP requires more than just sprinkling var_dump() or print_r() statements. Following best practices helps you save time, avoid mistakes, and maintain clean, maintainable code. Here’s a full explanation of the key practices:

1. Using Consistent Debug Formatting

Why it matters:

Consistent formatting makes it easy to read and compare debug output across multiple files, requests, or debugging sessions. Without consistency, you may waste time deciphering outputs or miss subtle bugs.

How to implement:

  1. Wrap output in <pre> for HTML:

echo '<pre>';
var_dump($data);
echo '</pre>';
  1. Include context and labels:

echo '<pre>';
echo "[After login] \$user data:\n";
print_r($user);
echo '</pre>';
  1. Use helper functions for standardized dumps:

function debug($var, $label = null) {
echo '<pre>';
if ($label) echo $label . "\n";
var_dump($var);
echo '</pre>';
}
  1. Sanitize sensitive data before printing:

$user['password'] = '***REDACTED***';
var_dump($user);
  1. Capture output for logs instead of printing directly:

$dump = print_r($user, true);
error_log($dump);

Benefits:

  • Makes debugging faster and more readable.

  • Prevents accidental leakage of sensitive information.

  • Helps when comparing outputs across multiple variables or sessions.

2. Debugging Step-by-Step

Why step-by-step debugging is important:

  • Jumping straight to large dumps can overwhelm you with data.

  • Small, controlled inspections make it easier to understand the flow of data and identify the root cause.

Techniques:

  1. Break your code into smaller chunks:

$data = fetchData();
debug($data, 'Fetched Data');

$processed = processData($data);
debug($processed, ‘Processed Data’);

  1. Inspect variables at key points:

  • After function calls

  • After loops

  • Before conditionals

  1. Use conditional debugging:

if ($user['id'] === 123) {
debug($user, 'Special User');
}
  1. Use slicing for large arrays:

debug(array_slice($bigArray, 0, 10), 'First 10 items');
  1. Check types when necessary:

var_dump($var); // distinguishes "5" (string) from 5 (int)

Benefits:

  • Reduces noise in debug output.

  • Helps locate the exact line causing the problem.

  • Makes debugging repeatable and reproducible.

3. Switching to Advanced Tools When Needed

Basic debugging functions are helpful, but for complex projects, large applications, or production environments, advanced tools are more efficient and safer.

Recommended advanced tools:

  1. Xdebug

    • Step-through debugging

    • Breakpoints, watches, stack traces

    • Profiling for performance analysis

  2. IDE Debuggers

    • PhpStorm, VSCode, NetBeans

    • Integrated with Xdebug or Zend Debugger

    • Visual inspection of variables, call stacks, and breakpoints

  3. Third-party debugging libraries

    • Symfony VarDumper: Color-coded, readable dumps

    • Kint: Interactive, collapsible output

    • Ray: Logs to a desktop app for visual inspection

  4. Logging frameworks

    • Monolog: Structured logging to files, databases, or external services

    • Ensures that debug information does not break user-facing output

When to switch:

  • Your codebase has complex nested arrays/objects.

  • Debugging production issues without exposing sensitive data.

  • Needing step-through inspection instead of static dumps.

  • Tracking performance, memory usage, or execution paths.

 

 

 

Transitioning to More Advanced Debugging Tools

Using var_dump() and print_r() is a great way to start debugging PHP code. But as your applications grow larger and more complex, these simple tools will eventually become limiting. This section explains when you need more advanced tools, introduces Xdebug, and explains how IDE debugging features make your workflow faster and more powerful.

1. When var_dump() and print_r() Are Not Enough

Basic output debugging is helpful for small issues, but it breaks down in real-world applications such as:

A) Large or deeply nested data

Dumping a huge array or object becomes unreadable and slows down your application.

B) Debugging performance or execution flow

var_dump() cannot show:

  • Which functions were called

  • The order of execution

  • How long each part of the code took

  • Memory usage
    All of these require profiling tools.

C) Debugging within loops or complex logic

Insertion of debugging output everywhere becomes messy, repetitive, and error-prone.

D) Debugging APIs, AJAX, CLI scripts

Adding debug output can corrupt:

  • JSON responses

  • XML feeds

  • Binary files
    Advanced tools avoid modifying the actual output.

E) Debugging in production

You cannot print sensitive information to users or logs.
You need advanced tools that work safely behind the scenes.

F) Finding the exact source of errors

Basic output shows the variable values but does not show:

  • Call stack

  • The file and line where the issue originated

  • Which function triggered the error

When you reach these limitations, it’s time to upgrade your debugging tools.


2. Introduction to Xdebug

Xdebug is the most powerful debugging extension for PHP.
It plugs directly into PHP and integrates with your IDE to give you deep insight into your code.

What Xdebug Can Do

Step Debugging

Pause (break) PHP execution and inspect:

  • Variables

  • Function parameters

  • Objects

  • Call stack
    right at the exact line where the code is running.

Breakpoints

Set breakpoints in your IDE:

// Click next to the line number to add a breakpoint
$user = getUser($id);

Execution stops here automatically.

Watch Variables

Automatically monitor changes to specific variables.

Stack Traces

Shows you the entire path your application took to reach the current line.

Profiling

Measure:

  • Execution time

  • Memory usage

  • Performance bottlenecks
    Profiling results can be viewed in tools like KCacheGrind or Webgrind.

Code Coverage

Helps you see how much of your code is covered by automated tests.

3. How Xdebug Improves Debugging

Compared to var_dump():

Feature var_dump() Xdebug
Shows variable value
Shows type/detail ✔ (better formatted)
Breakpoints
Step-by-step execution
Inspect variables without printing
No effect on output
Profiling
Code coverage

Xdebug gives you visibility into your application without modifying your code.

4. IDE Debugging Features

Most modern IDEs (PhpStorm, VSCode, NetBeans) support advanced debugging integrations. When combined with Xdebug, IDE debugging becomes incredibly powerful.

Common IDE Debugging Features

A) Step Through Code

Move through your program line by line:

  • Step Into (dive into function)

  • Step Over (skip into function body)

  • Step Out (return to caller)

B) Automatic Variable Display

The IDE shows all current variables in a sidebar:

  • Arrays

  • Objects

  • Types

  • Private/protected fields

  • Nested structures

No need for var_dump().

C) Call Stack Panel

See the exact path of function calls that led to the current state.

D) Conditional Breakpoints

Pause execution only if a condition is true:

$user->id == 53

Great for debugging loops.

E) Evaluate Expressions

You can run PHP expressions inside the debugger:

count($items)
$user->getPermissions()

F) Edit and Continue (in some IDEs)

Change your code while debugging without restarting the session.

5. When You Should Switch to Advanced Tools

Switch to Xdebug and IDE debugging when:

✔ The bug is difficult to reproduce
✔ The code is large or deeply nested
✔ You need to analyze program flow
✔ Adding dumps makes the output unreadable
✔ Debugging APIs or background jobs
✔ Debugging performance issues
✔ You want a faster, more professional workflow