PHP is a forgiving language — and that forgiveness is exactly what makes it dangerous in production.
Everything works fine locally. Code review passes. Then a silent bug surfaces on the live server, in front of real users, on a deadline.
These aren't bugs from documentation or textbooks. These are bugs that appear in login systems, e-commerce platforms, school portals, and client projects — bugs I've documented from real-time development experience.
Here are 10 of the most common ones, with the broken code, the fix, and a clear explanation of why it fails.
Bug #1 — Variable Not Accessible Inside a Function
// ❌ Broken
$username = "Rahul";
function greetUser() {
echo "Hello, " . $username; // Undefined variable
}
// ✅ Fixed — pass as parameter (cleaner approach)
function greetUser($username) {
echo "Hello, " . $username;
}
greetUser("Rahul");
Why it fails: PHP functions have isolated scope. Variables defined outside are not automatically available inside — unlike JavaScript. This silently breaks session handling and login systems.
Bug #2 — Assignment Instead of Comparison
// ❌ Broken — always true
if($isLoggedIn = true) {
echo "Welcome!";
}
// ✅ Fixed
if($isLoggedIn === true) {
echo "Welcome!";
}
Why it fails: = assigns a value. === compares value AND type. The condition evaluates the assigned value (true), so it's always true — regardless of actual login state. In production, this is an authentication bypass.
Bug #3 — strlen() on UTF-8 / Multibyte Text
// ❌ Broken
$text = "नमस्ते";
echo strlen($text); // Returns 18, not 6
// ✅ Fixed
echo mb_strlen($text, 'UTF-8'); // Returns 6
Why it fails: strlen() counts bytes, not characters. Each UTF-8 character can be 2–4 bytes. This silently breaks form validation, character limits, and SMS length checks for any non-ASCII content.
Same issue exists with
substr(),strpos(),strtolower()— always use theirmb_equivalents.
Bug #4 — Missing isset() on POST/GET Data
// ❌ Broken
echo $_POST['username']; // Undefined index on first load
// ✅ Fixed
if(isset($_POST['username'])) {
echo $_POST['username'];
}
// PHP 8+ shorthand
$name = $_POST['username'] ?? '';
Why it fails: $_POST is only populated on form submission. On first load, refresh, or direct URL access — the key doesn't exist. Every form handler needs this check.
Bug #5 — Storing Plain Text Passwords
// ❌ Never do this
$sql = "INSERT INTO users (password) VALUES ('$password')";
// ✅ Fixed
$hashed = password_hash($_POST['password'], PASSWORD_BCRYPT);
$stmt = $pdo->prepare("INSERT INTO users (password) VALUES (?)");
$stmt->execute([$hashed]);
// Verification
if(password_verify($inputPassword, $hashedFromDB)) {
echo "Login successful";
}
Why it fails: If your database is ever compromised, plain text passwords give attackers instant access to every account. password_hash() generates a strong salted hash. This is OWASP Top 10 — not optional.
Bug #6 — SQL Injection via Raw User Input
// ❌ Broken — wide open to injection
$id = $_GET['id'];
$result = mysqli_query($conn, "SELECT * FROM users WHERE id = $id");
// ?id=1 OR 1=1 dumps your entire table
// ✅ Fixed — PDO prepared statements
$stmt = $pdo->prepare("SELECT * FROM users WHERE id = ?");
$stmt->execute([$_GET['id']]);
$user = $stmt->fetch();
Why it fails: User input goes directly into the query string. A crafted input can read, modify, or delete your entire database. Prepared statements separate input from query structure completely — input is always treated as data, never as SQL.
Bug #7 — Header Redirect Without exit()
// ❌ Broken — script keeps running after redirect
if(!$isAdmin) {
header("Location: login.php");
deleteAllRecords(); // This WILL execute
}
// ✅ Fixed
if(!$isAdmin) {
header("Location: login.php");
exit();
}
Why it fails: header() sets the redirect but does not stop PHP execution. The browser leaves, but the server keeps running the rest of the script. Always add exit() after every redirect — especially in admin panels and access control logic.
Bug #8 — file_get_contents() on Large Files
// ❌ Broken — crashes on large files
$data = file_get_contents("students_data.csv"); // 50MB into RAM
$lines = explode("\n", $data);
// ✅ Fixed — stream line by line
$handle = fopen("students_data.csv", "r");
if($handle !== false) {
while(($line = fgets($handle)) !== false) {
$fields = str_getcsv($line);
// process one row at a time
}
fclose($handle);
}
Why it fails: file_get_contents() loads the entire file into memory. For large CSVs or data exports, PHP hits its memory limit and crashes. fgets() reads one line at a time — memory stays flat regardless of file size.
Bug #9 — session_start() After Output
// ❌ Broken
echo "Welcome!";
session_start(); // Error: headers already sent
// ✅ Fixed — session_start() must be first
<?php
session_start();
?>
<!DOCTYPE html>
<html>
<body>Welcome!</body>
</html>
Why it fails: PHP sends HTTP headers before any output. Once a single character is output — even whitespace before <?php — headers are locked. session_start() sets a cookie header, so it must come before any output. Hidden causes: BOM in file encoding, whitespace in included files.
Bug #10 — PDO Without Error Mode Set
// ❌ Broken — fails silently
$pdo = new PDO("mysql:host=localhost;dbname=mydb", $user, $pass);
$stmt = $pdo->query("SELECT * FROM non_existing_table");
// Returns false with no error — confusion guaranteed
// ✅ Fixed
try {
$pdo = new PDO(
"mysql:host=localhost;dbname=mydb",
$user,
$pass,
[
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC
]
);
} catch(PDOException $e) {
error_log("DB Error: " . $e->getMessage()); // Log it
die("Something went wrong."); // Generic message for user
}
Why it fails: PDO's default mode swallows errors silently. Setting ERRMODE_EXCEPTION turns every database failure into a catchable exception. Always log the real error, show users a generic message — never echo raw exception output in production.
Quick Reference
| # | Bug | Category | Risk |
|---|---|---|---|
| 1 | Variable scope in function | Logic | Medium |
| 2 |
= instead of ===
|
Type | Critical |
| 3 |
strlen() on UTF-8 text |
Type | Medium |
| 4 | Missing isset() on POST/GET |
Logic | Medium |
| 5 | Plain text password storage | Security | Critical |
| 6 | SQL Injection via raw input | Security | Critical |
| 7 | Redirect without exit()
|
Security | High |
| 8 |
file_get_contents() on large files |
Performance | High |
| 9 |
session_start() after output |
Logic | Medium |
| 10 | PDO without error mode | Database | High |
The Common Thread
Most of these share three root causes: unvalidated input, unhandled errors, and wrong assumptions about state. Build these three habits and you eliminate the majority of production PHP bugs:
- Always validate input
- Always escape output
- Always handle errors explicitly
Full article with deeper explanations: 10 Common PHP Bugs in Real-Time Development (With Fixes)
Found a bug we missed? Drop it in the comments — this list gets updated from real projects.
Top comments (0)