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:
- Reproduce the problem.
- State expected and actual behaviour.
- Reduce the input or request.
- Inspect the value at a useful boundary.
- Form one testable hypothesis.
- Change one thing.
- Verify the original case and nearby cases.
- 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
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:
5000pennies; - expected:
500pennies; - actual:
0pennies; - nearby passing input:
5001pennies.
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
$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
$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
$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
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
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:
- Which code produced the wrong output?
- What value did that code receive?
- Was the value already wrong at that point?
- Which earlier boundary created or transformed it?
- 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
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
falseor0; - 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
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
falsefrom valid integer0. - 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, and5001. - Remove temporary dumps from the final function.
- State why dumping this data into a JSON response would break the response.
Show solution
<?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
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.