Debugging PHP is rarely about one magic tool. For most teams, the real problem is slower: a bug appears, nobody can see enough context, and three people start guessing from different angles. That costs time, delays releases, and turns routine defects into expensive interruptions.
A better approach is simple. Make PHP show everything in development, log safely in production, add targeted breadcrumbs around the suspected failure, and use Xdebug when the control flow is too messy to reason about from logs alone. If you run an agency team or an in-house product, that discipline matters more than any single framework or hosting setup.
Start with visibility, not guesswork
The first job is to see what PHP is trying to tell you. For active debugging, use full error reporting rather than only fatal errors and warnings. The current PHP manual points teams toward E_ALL or -1 when you want the widest coverage.
<?php
error_reporting(E_ALL);
ini_set('display_errors', '1');
?>That is useful in local development, disposable staging environments, or a temporary investigation window. If your application fails before normal execution gets going, set the equivalent values in php.ini, a hosting panel, or your PHP-FPM or Apache configuration rather than relying only on code inside the app. If you are chasing early bootstrap problems, enable startup errors there as well.
display_errors = On
display_startup_errors = On
log_errors = On
error_reporting = E_ALLProduction is different. End users should not see stack traces, file paths, or configuration details. In live environments, keep errors logged but not displayed:
display_errors = Off
log_errors = On
error_log = /var/log/php/error.log
error_reporting = E_ALLThis separates diagnosis from customer experience. Owners and operations leads should treat that as basic hygiene: developers get the data they need, while the public site stays calm and professional.
Use logs to answer a specific question
Once visibility is on, stop scattering random debug output across templates and controllers. Good debugging logs answer a question: did the webhook arrive, which branch did checkout logic take, which customer record failed validation? If you log with that level of intent, you usually narrow the problem much faster.
<?php
error_log('[checkout] order=' . $orderId . ' state=' . $state);
error_log('[checkout] payload=' . json_encode($payload));
?>If you need a dedicated temporary file during an investigation, error_log() can append to one:
<?php
error_log('API response: ' . json_encode($response) . PHP_EOL, 3, __DIR__ . '/php-debug.log');
?>Keep the habit disciplined. Log identifiers, states, and decision points. Avoid dumping secrets, access tokens, or personal data unless you have a controlled reason and a plan to remove that output immediately after the investigation.
Dump variables deliberately
var_dump() is still useful. It remains the fastest way to inspect a value when you do not yet know whether the bug is in the data shape, the type, or the control flow. The mistake is not using var_dump(); the mistake is leaving noisy dumps spread across a codebase with no capture or cleanup plan.
<?php
var_dump($variable);
?>If you need to save the output for later comparison, capture it cleanly instead of sending it straight to the browser:
<?php
ob_start();
var_dump($variable);
$dump = ob_get_clean();
file_put_contents(__DIR__ . '/dump.txt', $dump);
?>This preserves the spirit of the old dump-to-file trick, but makes it safer and easier to reuse. Store the file outside the public web root if the data may contain credentials, customer data, or internal paths.
Bring in Xdebug for complex paths
Logs and dumps are usually enough for straightforward bugs. They stop being enough when the issue depends on branching logic, framework lifecycle hooks, or data being mutated across several layers. That is where Xdebug earns its keep.
For most teams, the practical starting point is step debugging plus development helpers:
xdebug.mode=debug,develop
xdebug.start_with_request=triggerThat lets you set breakpoints in your IDE, step through code, inspect variables, and avoid debugging every single request by default. In current Xdebug documentation, trigger mode is the default for step debugging. For CLI work or one-off scripts, you can trigger a session only when needed:
XDEBUG_TRIGGER=1 php your-script.phpIf your team is dealing with a recurring issue that only happens sometimes, Xdebug can save hours by showing the exact branch taken rather than forcing everyone to infer it from incomplete output. The main caution is operational: use it deliberately, not as a blanket setting on live traffic.
A simple debugging workflow that scales
- Reproduce the issue with the smallest possible input or scenario.
- Turn on full visibility in development, or safe logging in staging or production.
- Add one or two high-signal log lines around the suspected failure point.
- Inspect the actual data shape before changing business logic.
- Use Xdebug when the execution path is unclear or highly stateful.
- Remove temporary dumps and convert the lesson into a test, checklist, or monitoring rule.
That final step is what separates a quick patch from a better delivery process. The goal is not to become heroic at debugging. The goal is to make the next incident shorter, cheaper, and less disruptive.
If your team wants help tightening PHP debugging, logging, and delivery handoffs across development and operations, Greg can support both the setup and the working process around it.
Need help with this kind of work?
Discuss your PHP workflow with Greg Get in touch with Greg.