1. Reproduce it in the smallest possible case
A bug you cannot reproduce is a bug you cannot fix. Narrow it: which request, which user role, which data. Then reduce it — a failing test, a single script, a minimal case. If the bug is data-dependent, extract the offending row into a fixture.
For environment-dependent bugs (works locally, fails in production), the difference is your first suspect: PHP version, extension versions, server config, file permissions, opcache.
2. Isolate with logs and structured errors
Replace var_dump with structured logging that records the actual request context: method, URI, headers, session ID, and the backtrace. In production, log to a file or service — never the browser output. Enable error display nowhere near production; log everything, display nothing.
A 500 error with a generic message is a logging configuration problem, not a mystery. Turn on error_log, catch exceptions in a handler, and log the exception object including the stack trace.
// Never echo errors in production; log them with context
error_reporting(E_ALL);
ini_set("log_errors", "1");
ini_set("display_errors", "0");
set_exception_handler(function (Throwable $e) {
error_log(sprintf(
"%s in %s:%d -- %s %s",
$e->getMessage(),
$e->getFile(),
$e->getLine(),
$_SERVER["REQUEST_METHOD"] ?? "CLI",
$_SERVER["REQUEST_URI"] ?? ""
));
http_response_code(500);
echo "Internal server error.";
}); 3. Instrument, fix, and prove it with a test
Once you have the failing path reproduced and instrumented, the fix is usually small and boring. The important part is proving it: write a regression test that fails on the old behavior and passes on the new one. If the code has no test harness, that is part of the problem — add one.
Then remove the temporary instrumentation, run the full test suite, and check the logs for a clean pass. A bug fixed without a regression test is a bug that will be reintroduced.