DEV Community

Cover image for 10 PHP Bugs That Break Real Projects — And How to Fix Them (Part 2)
Bikki Singh
Bikki Singh

Posted on • Originally published at codepractice.in

10 PHP Bugs That Break Real Projects — And How to Fix Them (Part 2)

Every PHP developer has lived this moment. Code works perfectly on localhost. You push to production. Something silently breaks — no error, no log, just wrong output and a few wasted hours tracing the problem.

These are not textbook bugs. They come from real PHP projects — login systems, e-commerce stores, admin panels, school portals, and APIs. Bugs that pass code review because they look fine at first glance.

This is Part 2 covering Bug #11 through Bug #20. Each one includes the broken code, the correct fix, and a clear explanation of what actually goes wrong on a live server.


Bug #11 — Infinite Loop from a Missing Counter Increment

// ❌ Wrong
$i = 0;
while($i < 10) {
    echo $i;
    // forgot $i++
}

// ✅ Correct
$i = 0;
while($i < 10) {
    echo $i;
    $i++;
}
Enter fullscreen mode Exit fullscreen mode

A forgotten $i++ locks up the PHP process entirely. On a shared server, this takes down the application for all users until the request times out. If the loop also queries a database, you are now firing thousands of repeated queries. One missing line — enormous consequences.


Bug #12 — String Comparison Failing Because of Letter Case

// ❌ Wrong — "admin" or "ADMIN" won't match
if($_POST['role'] == "Admin") { ... }

// ✅ Correct
if(strtolower($_POST['role']) == "admin") { ... }
Enter fullscreen mode Exit fullscreen mode

PHP string comparisons are case-sensitive by default. Users submit "admin", "Admin", or "ADMIN" — all different strings to PHP. If your access control depends on this comparison, some users get incorrectly denied or incorrectly granted access. Normalize with strtolower() before comparing, and store values lowercase in the database too.


Bug #13 — Integer Cast Cutting Off Decimals in Division

// ❌ Wrong
$result = 5 / 2;
echo (int)$result; // Output: 2 (not 2.5)

// ✅ Correct
echo $result;                    // 2.5
echo number_format($result, 2); // 2.50
Enter fullscreen mode Exit fullscreen mode

PHP returns 2.5 for 5 / 2 — that is correct. But (int) truncates, it does not round. So 2.9 becomes 2, 9.99 becomes 9. In billing systems, GST calculations, or cart totals, this produces real financial errors on every invoice. Use round() for rounding and number_format() for display output.


Bug #14 — JSON Decode Returning an Object Instead of an Array

// ❌ Wrong
$data = json_decode($jsonString);
echo $data['name']; // Fatal error: Cannot use object as array

// ✅ Correct
$data = json_decode($jsonString, true);
echo $data['name']; // Works

// Also add error checking
if(json_last_error() !== JSON_ERROR_NONE) {
    error_log('JSON decode failed: ' . json_last_error_msg());
}
Enter fullscreen mode Exit fullscreen mode

By default json_decode() returns a stdClass object. You need -> notation for objects, [] for arrays — mixing them causes a fatal error. Pass true as the second argument to always get an associative array. API responses can also arrive malformed, so always check json_last_error() after decoding.


Bug #15 — Printing User Input Directly to HTML (XSS Vulnerability)

// ❌ Wrong
echo "Hello " . $_GET['name'];

// ✅ Correct
echo "Hello " . htmlspecialchars($_GET['name'], ENT_QUOTES, 'UTF-8');
Enter fullscreen mode Exit fullscreen mode

Echoing $_GET['name'] unescaped means an attacker can inject a <script> tag into your page. That script executes in every visitor's browser. A URL like ?name=<script>alert(document.cookie)</script> is all it takes.

htmlspecialchars() converts <, >, ", ', and & into safe HTML entities. Always use ENT_QUOTES and 'UTF-8'. This is non-negotiable — never print user input to HTML without it.


Bug #16 — Relative Include Paths Breaking on Different Servers

// ❌ Wrong — works locally, breaks on server
include("includes/header.php");

// ✅ Correct — always works
include(__DIR__ . "/includes/header.php");
Enter fullscreen mode Exit fullscreen mode

Relative paths resolve based on the current working directory — not the file's own directory. On localhost with XAMPP this might work. On a live Apache or Nginx server with different virtual host settings, the same path fails silently with a "No such file or directory" error.

__DIR__ is a PHP magic constant that always returns the absolute path of the current file's directory, regardless of where the script is called from.


Bug #17 — Duplicate Database Entries from Form Resubmission

// ❌ Wrong — direct insert, no duplicate check
$sql = "INSERT INTO users (email) VALUES ('$email')";

// ✅ Correct — check first, then insert
$stmt = $pdo->prepare("SELECT id FROM users WHERE email = ?");
$stmt->execute([$email]);

if(!$stmt->fetch()) {
    $insert = $pdo->prepare("INSERT INTO users (email) VALUES (?)");
    $insert->execute([$email]);
}
Enter fullscreen mode Exit fullscreen mode

Two real causes: users double-clicking the submit button on slow connections, and browsers resubmitting POST data on page refresh. Fix requires two layers — a PHP-side check before inserting, plus a database-level UNIQUE constraint so simultaneous requests cannot both slip through:

ALTER TABLE users ADD UNIQUE (email);
Enter fullscreen mode Exit fullscreen mode

After a successful submission, always redirect: header("Location: success.php"); exit;


Bug #18 — array_merge Resetting Numeric Keys

$a = [1 => "one", 2 => "two"];
$b = [2 => "TWO", 3 => "three"];

// ❌ Wrong — keys reset to 0, 1, 2, 3
print_r(array_merge($a, $b));

// ✅ Correct — preserve keys (left side wins on conflict)
$result = $a + $b;

// ✅ Or — right side overwrites left on conflict
$result = array_replace($a, $b);
Enter fullscreen mode Exit fullscreen mode

array_merge() reindexes all numeric keys from zero. If you are merging config arrays or working with database results keyed by IDs, this silently destroys your key structure without any warning or error. The + operator preserves keys; array_replace() preserves keys and lets the right array win on conflicts.


Bug #19 — No Error Logging Set Up in Production

// ❌ Wrong — errors disappear completely
// php.ini: display_errors = Off
// (no error_log path configured)

// ✅ Correct

// Development:
ini_set('display_errors', 1);
error_reporting(E_ALL);

// Production:
ini_set('display_errors', 0);
ini_set('log_errors', 1);
ini_set('error_log', '/var/log/php/error.log');
error_reporting(E_ALL);
Enter fullscreen mode Exit fullscreen mode

Turning off display_errors in production is correct — you do not want stack traces visible to users or attackers. But many developers stop there. The result: errors happen silently, users complain, and you have zero record of what broke or when. Always configure log_errors and set an error_log path. Tools like Sentry or Bugsnag give you real-time alerts with full stack traces — the professional standard for production monitoring.


Bug #20 — foreach Not Modifying the Original Array

// ❌ Wrong — only modifies a copy
$prices = [100, 200, 300];
foreach($prices as $price) {
    $price = $price * 1.18;
}
print_r($prices); // Still [100, 200, 300]

// ✅ Correct — reference with &
foreach($prices as &$price) {
    $price = $price * 1.18;
}
unset($price); // ⚠️ Critical — must unset after the loop
print_r($prices); // [118, 236, 354]

// ✅ Alternative — array_map (no reference needed)
$prices = array_map(fn($p) => $p * 1.18, $prices);
Enter fullscreen mode Exit fullscreen mode

foreach works on a copy of each element by default. The & makes it a reference to the actual array element. The unset($price) after the loop is not optional — after the loop, $price still points to the last element. Any later use of that variable name in the same scope will silently corrupt your array. This bug can live in a codebase for months before anyone notices.


Quick Reference

Bug Issue Risk
#11 Infinite loop Server crash
#12 Case-sensitive comparison Access control failure
#13 Decimal truncated to integer Financial error
#14 JSON returns object not array Fatal error
#15 XSS vulnerability Security breach
#16 Include path breaks on server Fatal include error
#17 Duplicate database records Data corruption
#18 Array keys reset after merge Lost key structure
#19 Errors invisible in production Silent failures
#20 Original array not modified Wrong data processed

What These Bugs Have in Common

Most of them do not throw errors. PHP's forgiving nature means the code runs — it just does not do the right thing. No warning. No crash. Just silent wrong behavior that costs hours to trace. That is what makes them dangerous in production.

The developers who catch these early are the ones who have been burned by them before, or who studied real-world bugs before getting burned. Syntax knowledge is not enough — understanding failure modes is what separates junior code from production-ready code.


Read the full article with detailed explanations:
👉 10 PHP Bugs That Break Real Projects — And How to Fix Them (Part 2)
2

Part 3 covers bugs #21–30 — session handling, file upload security, date/timezone bugs, and database connection errors.

Top comments (0)