Code Quality And Tooling

Debugging With Evidence And `var_dump()`

Debugging is the process of finding where actual program state first differs from expected state. It is not changing several lines until the visible symptom disappears.

A reliable loop is:

  1. Reproduce the problem.
  2. State expected and actual behaviour.
  3. Reduce the input or request.
  4. Inspect the value at a useful boundary.
  5. Form one testable hypothesis.
  6. Change one thing.
  7. Verify the original case and nearby cases.
  8. Add a lasting test when the bug could return.

Reproduce The Smallest Failing Case

Suppose the business rule is "GBP 50 or more receives a GBP 5 discount":

PHP example
<?php

declare(strict_types=1);

function discountForSubtotal(int $subtotal): int
{
    if ($subtotal > 5000) {
        return 500;
    }

    return 0;
}

echo discountForSubtotal(5000);

The smallest useful report is not "discounts are broken." It is:

  • input: 5000 pennies;
  • expected: 500 pennies;
  • actual: 0 pennies;
  • nearby passing input: 5001 pennies.

That points directly at a boundary comparison.

Read What var_dump() Tells You

var_dump() writes diagnostic output containing a value's PHP type and representation. For strings it also reports the byte length; for arrays and objects it recursively displays their contents.

PHP example
<?php

$quantity = '2';
$active = false;
$items = ['A12', 'B07'];

var_dump($quantity, $active, $items);

// string(1) "2"
// bool(false)
// array(2) {
//   [0]=>
//   string(3) "A12"
//   [1]=>
//   string(3) "B07"
// }

The type is often the important clue. HTML form values normally arrive as strings, so string(1) "2" is different from int(2). An empty array is different from null. The string "false" is not the boolean false.

var_dump() returns no useful inspected value; it writes output as a side effect. Use it on a separate line rather than embedding it in an expression.

Dump At Boundaries, Not Everywhere

Choose locations where data changes form or ownership:

  • immediately after request input is read;
  • after validation and conversion;
  • before and after a calculation;
  • after fetching a database row;
  • before calling an external API;
  • when a function receives an unexpected argument;
  • before constructing the response.

For example:

PHP example
<?php

$rawSubtotal = $_POST['subtotal'] ?? null;
var_dump($rawSubtotal);

$subtotal = filter_var($rawSubtotal, FILTER_VALIDATE_INT);
var_dump($subtotal);

Possible output is:

string(4) "5000"
int(5000)

If validation fails, filter_var() returns false. Because 0 can be a valid integer, check with === false rather than a loose truthiness test.

PHP example
<?php

$subtotal = filter_var($rawSubtotal, FILTER_VALIDATE_INT);

if ($subtotal === false || $subtotal < 0) {
    throw new InvalidArgumentException('Subtotal must be a non-negative integer.');
}

The first dump establishes what entered PHP. The second establishes whether conversion produced the expected domain type. Dumping ten unrelated variables would make the evidence harder to read.

Understand Where Dump Output Goes

In a command-line script, var_dump() writes into terminal output. In a web request, it writes into the HTTP response body. That can:

  • corrupt JSON, XML, image, or file-download responses;
  • send output before headers and trigger header errors;
  • expose paths, tokens, personal data, or internal object state;
  • break redirects or automated clients;
  • produce difficult-to-read browser output.

For browser-only local debugging, wrapping output in <pre> may improve formatting, but it does not make the data safe:

PHP example
<?php

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

Never expose diagnostic dumps to real users. Do not dump entire request, session, environment, payment, authentication, or user objects when a single non-sensitive field is enough.

When direct response output is unsuitable, use the application's logger with structured context. Redact secrets and personal data, and remove temporary high-volume logging after the problem is understood.

Compare Other Inspection Tools

PHP provides related tools with different purposes:

  • print_r($value) produces a human-readable structure but gives less precise type information.
  • print_r($value, true) returns the representation instead of printing it.
  • var_export($value, true) returns a PHP-like representation useful for some scalar and array diagnostics.
  • get_debug_type($value) returns a concise type name without dumping contents.
  • debug_backtrace() reports the call path, but large traces should be limited and handled carefully.

For a suspected string-versus-integer bug, var_dump() is usually better than print_r() because it makes the type visible:

PHP example
<?php

print_r('5000');
// 5000

var_dump('5000');
// string(4) "5000"

For a large object graph, dumping the entire object can be noisy or recursive. Inspect a specific property, identifier, count, or type instead.

Follow The Wrong Value Backwards

A wrong page total does not prove the template is faulty. The value may have become wrong during input conversion, calculation, database retrieval, serialization, or formatting.

Work backwards from the symptom:

  1. Which code produced the wrong output?
  2. What value did that code receive?
  3. Was the value already wrong at that point?
  4. Which earlier boundary created or transformed it?
  5. What is the earliest point where expected and actual state differ?

Fix that origin. Formatting 0 as 500 in the view would hide the calculation bug and create another inconsistency.

Change One Thing And Check The Boundary

The discount example needs >=:

PHP example
<?php

declare(strict_types=1);

function discountForSubtotal(int $subtotal): int
{
    if ($subtotal >= 5000) {
        return 500;
    }

    return 0;
}

var_dump(discountForSubtotal(4999));
var_dump(discountForSubtotal(5000));
var_dump(discountForSubtotal(5001));

// int(0)
// int(500)
// int(500)

Checking below, at, and above the threshold guards against replacing one off-by-one error with another.

Replace Temporary Output With Lasting Proof

Before finishing:

  • remove temporary var_dump(), print_r(), and trace calls;
  • check the response remains valid;
  • search the diff for debug statements;
  • add a regression test for the failing input;
  • document how the fix was verified.

A focused test might assert 4999, 5000, and 5001. The dump helped discover the defect; the test prevents it from quietly returning.

Common Failure Patterns

Avoid these habits:

  • changing several variables before reproducing again;
  • assuming input types instead of inspecting them;
  • using loose comparisons after validation returns false or 0;
  • dumping huge objects when one field answers the question;
  • exposing diagnostic output or secrets in production;
  • treating the last stack-frame line as the root cause;
  • removing validation because it revealed invalid data;
  • fixing presentation instead of the first wrong value;
  • committing debug output instead of a regression test.

Methodical debugging is faster because every inspection answers a question. var_dump() is valuable when it provides precise evidence at a chosen boundary, and harmful when it becomes uncontrolled output scattered through the application.

Practice

Task: Debug Input Type And Discount Boundaries
PHP example
<?php

declare(strict_types=1);

function discountForSubmittedSubtotal(mixed $rawSubtotal): int
{
    $subtotal = filter_var($rawSubtotal, FILTER_VALIDATE_INT);

    if (!$subtotal || $subtotal < 0) {
        throw new InvalidArgumentException('Subtotal must be a non-negative integer.');
    }

    if ($subtotal > 5000) {
        return 500;
    }

    return 0;
}

Use focused var_dump() calls during diagnosis to inspect the raw and validated values for '0', '4999', '5000', '5001', and 'not-a-number'.

Requirements

  • Explain why form input '5000' is not initially an integer.
  • Distinguish validation failure false from valid integer 0.
  • Fix the discount boundary for "GBP 50 or more."
  • Keep all calculations in integer pennies.
  • Reject negative and non-integer input with the existing exception type.
  • Verify expected results for 0, 4999, 5000, and 5001.
  • Remove temporary dumps from the final function.
  • State why dumping this data into a JSON response would break the response.
Show solution
PHP example
<?php

foreach (['0', '4999', '5000', '5001', 'not-a-number'] as $rawSubtotal) {
    $subtotal = filter_var($rawSubtotal, FILTER_VALIDATE_INT);

    var_dump($rawSubtotal, $subtotal);
}

The raw form values are strings. Valid numeric strings become integers, including int(0), while 'not-a-number' becomes bool(false). The original if (!$subtotal) treated both 0 and false as false-like, so it rejected a valid zero subtotal.

The final function uses a strict validation check and fixes the threshold:

PHP example
<?php

declare(strict_types=1);

function discountForSubmittedSubtotal(mixed $rawSubtotal): int
{
    $subtotal = filter_var($rawSubtotal, FILTER_VALIDATE_INT);

    if ($subtotal === false || $subtotal < 0) {
        throw new InvalidArgumentException('Subtotal must be a non-negative integer.');
    }

    if ($subtotal >= 5000) {
        return 500;
    }

    return 0;
}

var_dump(discountForSubmittedSubtotal('0'));
var_dump(discountForSubmittedSubtotal('4999'));
var_dump(discountForSubmittedSubtotal('5000'));
var_dump(discountForSubmittedSubtotal('5001'));

// int(0)
// int(0)
// int(500)
// int(500)

The verification dumps belong in a local diagnostic script or should become assertions in an automated test. They should not remain in the response path. var_dump() writes directly to the response body, so output before a JSON document would make the body invalid JSON and could also expose internal values.