Database Connectivity 7 programs with output 15 Q/A 5 MCQs

Learning Objectives – after this chapter you will be able to:

  • Explain SQL injection and demonstrate how concatenated queries can be attacked.
  • Use prepared statements in procedural and object-oriented style: prepare, bind, execute, fetch.
  • Choose the correct bind types i, d, s and b, and use execute_query() (PHP 8.2).
  • Group several statements into a transaction with commit and rollback.
  • Store passwords safely with password_hash() and build a secure registration and login.
SQL Injection

SQL injection is one of the most common and most dangerous attacks on web applications. It happens when user input is pasted directly into an SQL statement, so that the input can change the meaning of the statement.

The following login page checks the user name and password by building the SQL statement with string concatenation. (It also stores a plain-text password – a second mistake, corrected later.)

File: htdocs/phpbook/ch36/badlogin.php
<?php
$conn = mysqli_connect("localhost", "root", "", "phpbook");
$user = $_POST['user'];
$pass = $_POST['pass'];
$sql = "SELECT * FROM users WHERE username = '$user' AND password = '$pass'";
echo "SQL sent to the server: " . htmlspecialchars($sql) . "<br>";
$result = mysqli_query($conn, $sql);
if (mysqli_num_rows($result) > 0) {
    $row = mysqli_fetch_assoc($result);
    echo "<b>Welcome {$row['username']} (role: {$row['role']})</b>";
} else {
    echo "Invalid login";
}
?>
OUTPUT browser · POST badlogin.php (user=admin, pass=wrong)
SQL sent to the server: SELECT * FROM users WHERE username = 'admin' AND password = 'wrong'
Invalid login

Now the attacker types ' OR '1'='1 as the password:

OUTPUT browser · POST badlogin.php (pass = ' OR '1'='1)
SQL sent to the server: SELECT * FROM users WHERE username = 'admin' AND password = '' OR '1'='1'
Welcome admin (role: admin)

The quote in the input closed the string, and OR '1'='1' made the condition always true – the attacker is logged in as admin without knowing the password. Similar input can read other tables, change data or delete it.

Prepared Statements

A prepared statement separates the SQL code from the data:

  1. Prepare – the SQL is sent to the server with placeholders ? instead of values. The server parses and checks it.
  2. Bind – the PHP variables are bound to the placeholders, together with their types.
  3. Execute – the values are sent separately. They are always treated as data, never as SQL code, so injection is impossible. The same prepared statement can be executed many times with different values, which is also faster.
Bind types
TypeMeaning
iinteger
ddouble (float / decimal)
sstring (also used for dates and decimals given as text)
bblob (binary data, sent in packets)

The type string has one letter per placeholder, e.g. "sis" for string, integer, string.

Prepared Statements – Procedural Style
FunctionPurpose
mysqli_prepare($conn, sql)Prepares a statement with ? placeholders; returns a statement object.
mysqli_stmt_bind_param($stmt, types, vars...)Binds variables to the placeholders.
mysqli_stmt_execute($stmt [, params])Executes the statement (PHP 8.1: values may be passed as an array).
mysqli_stmt_get_result($stmt)Gets a result set (mysqli_result) from a SELECT.
mysqli_stmt_bind_result($stmt, vars...) / mysqli_stmt_fetch($stmt)Binds result columns to variables / fetches the next row into them.
mysqli_stmt_affected_rows($stmt) / mysqli_stmt_insert_id($stmt)Rows changed / last insert id.
mysqli_stmt_close($stmt)Closes the statement.
File: htdocs/phpbook/ch36/goodlogin_proc.php
<?php
$conn = mysqli_connect("localhost", "root", "", "phpbook");
$stmt = mysqli_prepare($conn,
    "SELECT username, role FROM users WHERE username = ? AND password = ?");
mysqli_stmt_bind_param($stmt, "ss", $_POST['user'], $_POST['pass']);
mysqli_stmt_execute($stmt);
$result = mysqli_stmt_get_result($stmt);
if ($row = mysqli_fetch_assoc($result)) {
    echo "Welcome {$row['username']}";
} else {
    echo "Invalid login – the injection attempt failed.";
}
mysqli_stmt_close($stmt);
mysqli_close($conn);
?>
OUTPUT browser · POST goodlogin_proc.php (pass = ' OR '1'='1)
Invalid login – the injection attempt failed.
Inserting several rows with one prepared statement
File: htdocs/phpbook/ch36/insert_proc.php
<?php
$conn = mysqli_connect("localhost", "root", "", "phpbook");
$stmt = mysqli_prepare($conn, "INSERT INTO fees (roll, amount, paid_on) VALUES (?, ?, ?)");
mysqli_stmt_bind_param($stmt, "ids", $roll, $amount, $date);   // bound by reference
$payments = [[101, 2250.00, "2026-07-01"], [102, 3000.00, "2026-07-05"], [101, 2250.00, "2026-08-01"]];
foreach ($payments as [$roll, $amount, $date]) {
    mysqli_stmt_execute($stmt);                                 // uses the current values
    echo "Saved payment id " . mysqli_stmt_insert_id($stmt) . " for roll $roll<br>";
}
mysqli_stmt_close($stmt);

// SELECT with bind_result
$stmt = mysqli_prepare($conn, "SELECT paid_on, amount FROM fees WHERE roll = ? ORDER BY paid_on");
$r = 101;
mysqli_stmt_bind_param($stmt, "i", $r);
mysqli_stmt_execute($stmt);
mysqli_stmt_bind_result($stmt, $paidOn, $amt);
echo "<b>Payments of roll $r</b><br>";
while (mysqli_stmt_fetch($stmt)) {
    echo "$paidOn : Rs. $amt<br>";
}
mysqli_stmt_close($stmt);
mysqli_close($conn);
?>
OUTPUT browser · http://localhost/phpbook/ch36/insert_proc.php
Saved payment id 1 for roll 101
Saved payment id 2 for roll 102
Saved payment id 3 for roll 101
Payments of roll 101
2026-07-01 : Rs. 2250.00
2026-08-01 : Rs. 2250.00

mysqli_stmt_bind_param() binds references to the variables, so each execute sends the values the variables have at that moment.

Prepared Statements – Object-Oriented Style
Method / propertyPurpose
$conn->prepare(sql)Prepares a statement; returns a mysqli_stmt object.
$stmt->bind_param(types, vars...)Binds variables to the placeholders.
$stmt->execute([params])Executes (PHP 8.1: an array of values may be passed directly).
$stmt->get_result()Returns a mysqli_result for a SELECT.
$stmt->bind_result(vars...) / $stmt->fetch()Binds result columns / fetches the next row.
$stmt->affected_rows / $stmt->insert_id / $stmt->num_rowsRows changed / last insert id / rows (after store_result()).
$stmt->close()Closes the statement.
$conn->execute_query(sql, params)PHP 8.2: prepares, binds and executes in one call; returns a result.
File: htdocs/phpbook/ch36/search_oop.php
<?php
$conn = new mysqli("localhost", "root", "", "phpbook");
$stmt = $conn->prepare("SELECT roll, name FROM students WHERE city = ? AND course_id = ?");
$stmt->bind_param("si", $_GET['city'], $_GET['course']);
$stmt->execute();
$result = $stmt->get_result();
echo "Found {$result->num_rows} student(s):<br>";
while ($row = $result->fetch_assoc()) {
    echo "{$row['roll']} – {$row['name']}<br>";
}
$stmt->close();

// PHP 8.1: pass the values to execute() as an array – no bind_param needed
$stmt = $conn->prepare("UPDATE courses SET fee = ? WHERE course_name = ?");
$stmt->execute([4800, "Core PHP"]);
echo "execute([...]): {$stmt->affected_rows} course updated<br>";

// PHP 8.2: execute_query() does prepare + bind + execute in one call
$row = $conn->execute_query("SELECT course_name, fee FROM courses WHERE course_name = ?", ["Core PHP"])
            ->fetch_assoc();
echo "execute_query: {$row['course_name']} now costs Rs. {$row['fee']}";
$conn->close();
?>
OUTPUT browser · http://localhost/phpbook/ch36/search_oop.php?city=Bhopal&course=1
Found 2 student(s):
101 – Asha Verma
103 – Neha Joshi
execute([...]): 1 course updated
execute_query: Core PHP now costs Rs. 4800.00
Placeholders are for values only

A ? can stand only for a value – not for a table name, column name, ORDER BY direction or SQL keyword. If such parts must depend on user input, check them against a whitelist, e.g. $order = in_array($_GET['sort'], ['name', 'city']) ? $_GET['sort'] : 'name';

Transactions

A transaction groups several SQL statements into one unit of work: either all of them succeed (commit) or none of them takes effect (rollback). Transactions need a transaction-safe engine such as InnoDB. They provide the ACID guarantees – Atomicity, Consistency, Isolation and Durability.

StepProceduralOOP
Startmysqli_begin_transaction($c)$c->begin_transaction()
Make permanentmysqli_commit($c)$c->commit()
Undomysqli_rollback($c)$c->rollback()
Auto-commit on/offmysqli_autocommit($c, false)$c->autocommit(false)

The example moves money from one account to another. Both updates must happen together: if the second one fails, the first must be undone.

File: htdocs/phpbook/ch36/transfer_oop.php
<?php
$conn = new mysqli("localhost", "root", "", "phpbook");
$amount = (float) ($_GET['amount'] ?? 0);

function balances(mysqli $conn): string
{
    $out = [];
    foreach ($conn->query("SELECT account, balance FROM ledger") as $r) {
        $out[] = "{$r['account']} = {$r['balance']}";
    }
    return implode(", ", $out);
}
echo "Before: " . balances($conn) . "<br>";
$conn->begin_transaction();
try {
    $stmt = $conn->prepare("UPDATE ledger SET balance = balance + ? WHERE account = ?");
    $stmt->execute([$amount, "Institute"]);             // credit
    $stmt->execute([-$amount, "Asha"]);                 // debit – fails if balance < 0
    $conn->commit();
    echo "Transfer of Rs. $amount committed.<br>";
} catch (mysqli_sql_exception $e) {
    $conn->rollback();
    echo "Transfer of Rs. $amount failed and was rolled back: " . $e->getMessage() . "<br>";
}
echo "After: " . balances($conn);
?>
OUTPUT browser · transfer_oop.php?amount=5000 (after a successful transfer of 1000)
Before: Institute = 51000.00, Asha = 2000.00
Transfer of Rs. 5000 failed and was rolled back: CONSTRAINT `ledger.balance` failed for `phpbook`.`ledger`
After: Institute = 51000.00, Asha = 2000.00

The first request transferred 1000 (Asha 3000 → 2000). The second asks for 5000; the credit to the institute was executed, but the debit violated the CHECK (balance >= 0) rule, so the whole transaction was rolled back and the institute's balance is unchanged.

The same in procedural style, used to record an admission and its first fee payment together:

File: htdocs/phpbook/ch36/admission_proc.php
<?php
$conn = mysqli_connect("localhost", "root", "", "phpbook");
mysqli_begin_transaction($conn);
try {
    $s1 = mysqli_prepare($conn, "INSERT INTO students (roll, name, city, course_id) VALUES (?, ?, ?, ?)");
    mysqli_stmt_execute($s1, [107, "Rohit Jain", "Bhopal", 2]);
    $s2 = mysqli_prepare($conn, "INSERT INTO fees (roll, amount, paid_on) VALUES (?, ?, CURDATE())");
    mysqli_stmt_execute($s2, [107, 3000]);
    mysqli_commit($conn);
    echo "Admission and first payment saved together.<br>";
} catch (mysqli_sql_exception $e) {
    mysqli_rollback($conn);
    echo "Nothing saved: " . $e->getMessage() . "<br>";
}
// try again with the same roll number – the duplicate makes the first INSERT fail
mysqli_begin_transaction($conn);
try {
    mysqli_stmt_execute(mysqli_prepare($conn, "INSERT INTO fees (roll, amount, paid_on) VALUES (?, ?, CURDATE())"), [107, 500]);
    mysqli_stmt_execute(mysqli_prepare($conn, "INSERT INTO students (roll, name) VALUES (?, ?)"), [107, "Duplicate"]);
    mysqli_commit($conn);
} catch (mysqli_sql_exception $e) {
    mysqli_rollback($conn);
    echo "Second attempt rolled back: " . $e->getMessage() . "<br>";
}
$n = mysqli_fetch_row(mysqli_query($conn, "SELECT COUNT(*) FROM fees WHERE roll = 107"))[0];
echo "Fee records for roll 107: $n";
?>
OUTPUT browser · http://localhost/phpbook/ch36/admission_proc.php
Admission and first payment saved together.
Second attempt rolled back: Duplicate entry '107' for key 'roll'
Fee records for roll 107: 1

The fee row of the second attempt was rolled back too, so roll 107 still has exactly one payment.

Storing Passwords Safely

Never store passwords in plain text, and never with md5() or sha1(). Use password_hash() – it generates a random salt and a slow bcrypt (or Argon2) hash – and check a login with password_verify(). The hash is about 60 characters long, so the column should be VARCHAR(255).

Secure Registration and Login (OOP)
File: htdocs/phpbook/ch36/register.php
<?php
$conn = new mysqli("localhost", "root", "", "phpbook");
$msg = "";
if ($_SERVER["REQUEST_METHOD"] == "POST") {
    $user = trim($_POST["user"]);
    $pass = $_POST["pass"];
    if (!preg_match("/^[a-z0-9_]{3,30}$/i", $user) || strlen($pass) < 8) {
        $msg = "User name 3–30 letters/digits, password at least 8 characters.";
    } else {
        try {
            $hash = password_hash($pass, PASSWORD_DEFAULT);
            $conn->execute_query("INSERT INTO users (username, password) VALUES (?, ?)", [$user, $hash]);
            $msg = "Registered successfully. You can now log in.";
        } catch (mysqli_sql_exception $e) {
            $msg = ($e->getCode() == 1062) ? "This user name is already taken." : "Registration failed.";
        }
    }
}
?>
<form method="post" style="font-family:Arial;font-size:13px">
  <?= htmlspecialchars($msg) ?><br>
  User <input name="user" id="u"> Password <input type="password" name="pass" id="p">
  <input type="submit" value="Register" id="r">
</form>
OUTPUT browser · register.php – new user kavita
Registered successfully. You can now log in.
User  Password
OUTPUT browser · register.php – the same user name again
This user name is already taken.
User  Password
File: htdocs/phpbook/ch36/login.php
<?php
session_start();
$conn = new mysqli("localhost", "root", "", "phpbook");
$row = $conn->execute_query("SELECT id, username, password, role FROM users WHERE username = ?",
                            [$_POST["user"] ?? ""])->fetch_assoc();
if ($row && password_verify($_POST["pass"] ?? "", $row["password"])) {
    session_regenerate_id(true);
    $_SESSION["user_id"] = $row["id"];
    $_SESSION["role"] = $row["role"];
    echo "Login successful – welcome {$row['username']} ({$row['role']})";
} else {
    echo "Invalid user name or password";
}
?>
OUTPUT browser · POST login.php (correct password)
Login successful – welcome kavita (reader)
OUTPUT browser · POST login.php (injection attempt)
Invalid user name or password

The stored value is a hash, not the password:

SQL SESSION SQL typed in the MariaDB monitor (XAMPP Shell → mysql -u root) or the phpMyAdmin SQL tab
MariaDB [phpbook]> SELECT username, LEFT(password, 29) AS start_of_hash, LENGTH(password) AS len FROM users WHERE username = 'kavita';
+----------+-------------------------------+-----+
| username | start_of_hash                 | len |
+----------+-------------------------------+-----+
| kavita   | $2y$10$UnTMvmel8SFIqt0nVdrJC. |  60 |
+----------+-------------------------------+-----+
1 row in set
Security Checklist for PHP and MySQLi
  • Use prepared statements (or execute_query()) for every query that contains outside data.
  • Validate input (types, ranges, formats – Chapters 12 and 21) and escape output with htmlspecialchars().
  • Hash passwords with password_hash(); regenerate the session id after login.
  • Connect with a limited database user, not root; keep credentials outside htdocs.
  • Show friendly error messages to users; log technical details (display_errors = Off in production).
  • Use transactions when several statements must succeed or fail together.
Key Takeaways
  • SQL injection happens when user input is concatenated into SQL; ' OR '1'='1 can bypass a login.
  • Prepared statements send SQL and data separately: prepare → bind (i d s b) → execute → fetch.
  • Procedural: mysqli_prepare, mysqli_stmt_bind_param, mysqli_stmt_execute, mysqli_stmt_get_result.
  • OOP: $conn->prepare(), $stmt->bind_param(), $stmt->execute(), $stmt->get_result(); PHP 8.2 $conn->execute_query().
  • Transactions: begin_transaction(), commit(), rollback() – all or nothing (InnoDB).
  • Store passwords with password_hash() and check them with password_verify().
Program File

Extract the ZIP into C:\xampp\htdocs. It creates the folder phpbook/ch36 used in the file labels above; open the programs at http://localhost/phpbook/… with Apache (and MySQL, where needed) started in the XAMPP Control Panel.

Question / Answer (15 questions)

Answer : An attack in which input supplied by a user is inserted into an SQL statement in such a way that it changes the statement's meaning, allowing the attacker to bypass logins or read, change or delete data.

Answer : The first quote closes the password string, and OR '1'='1' adds a condition that is always true, so the WHERE clause matches rows even though the password is wrong.

Answer : An SQL statement with ? placeholders that is sent to the server first and executed later with values sent separately, so the values can never be interpreted as SQL code.

Answer : Prepare the SQL with placeholders, bind the variables (with their types), and execute; for SELECT then fetch the results.

Answer : i = integer, d = double (float), s = string, b = blob (binary data); the type string contains one letter per placeholder.

Answer : $stmt = mysqli_prepare($conn, $sql); mysqli_stmt_bind_param($stmt, "si", $a, $b); mysqli_stmt_execute($stmt); $result = mysqli_stmt_get_result($stmt);

Answer : $stmt = $conn->prepare($sql); $stmt->bind_param("si", $a, $b); $stmt->execute(); $result = $stmt->get_result();

Answer : get_result() returns a mysqli_result object that can be fetched as arrays or objects; bind_result() binds each result column to a variable, and fetch() fills those variables row by row.

Answer : PHP 8.1 lets execute() receive the values as an array (no bind_param needed); PHP 8.2 added mysqli_execute_query() / $conn->execute_query(), which prepares, binds and executes in one call.

Answer : No – placeholders are only for values. Table/column names or sort directions from user input must be checked against a whitelist of allowed values.

Answer : A group of SQL statements executed as one unit: either all of them are made permanent with commit, or all are undone with rollback.

Answer : Atomicity (all or nothing), Consistency (data stays valid), Isolation (concurrent transactions do not interfere) and Durability (committed data survives crashes).

Answer : begin_transaction(), commit() and rollback() (procedural: mysqli_begin_transaction(), mysqli_commit(), mysqli_rollback()); autocommit(false) also starts manual transaction mode.

Answer : password_hash() uses a slow, salted algorithm (bcrypt/Argon2) designed for passwords, so each hash is unique and brute-force attacks are very slow; md5() is fast, unsalted and easily cracked.

Answer : Fetch the stored hash for the user name with a prepared statement and call password_verify($typedPassword, $storedHash), which returns true if they match.
MCQ Test : Check Your Understanding (5 questions)

Select an option and press Check Answer, or answer all questions and press Submit Test to see your score. Each answer comes with an explanation.

Q1. Prepared statements protect against:

Show Answer

Correct Answer : B. SQL injection
Data is sent separately from the SQL code.

Q2. In bind_param("sid", $a, $b, $c), $b is bound as:

Show Answer

Correct Answer : B. An integer
The second letter, i, means integer.

Q3. Which PHP 8.2 method prepares, binds and executes in one call?

Show Answer

Correct Answer : B. $conn->execute_query()
execute_query() takes the SQL and an array of values.

Q4. Which method undoes all statements of the current transaction?

Show Answer

Correct Answer : B. rollback()
rollback() cancels the uncommitted changes.

Q5. Which function checks a password against a stored hash?

Show Answer

Correct Answer : C. password_verify()
password_verify() compares a password with a hash.

Bibliography/References

The notes on this page are the author’s own compilation from the e-book PHP: Basics to Advance. Every program was run on Apache with PHP 8 and MariaDB, and the output shown is the real result of that run. The works listed below were consulted and are cited for attribution and further reading. Product names and trademarks belong to their respective owners.

  1. The PHP Group. (n.d.). PHP Manual – Prepared Statements (MySQLi). https://www.php.net/manual/en/mysqli.quickstart.prepared-statements.php
  2. The PHP Group. (n.d.). PHP Manual – mysqli_stmt::bind_param. https://www.php.net/manual/en/mysqli-stmt.bind-param.php
  3. The PHP Group. (n.d.). PHP Manual – mysqli::execute_query. https://www.php.net/manual/en/mysqli.execute-query.php
  4. The PHP Group. (n.d.). PHP Manual – mysqli::begin_transaction. https://www.php.net/manual/en/mysqli.begin-transaction.php
  5. The PHP Group. (n.d.). PHP Manual – password_hash. https://www.php.net/manual/en/function.password-hash.php
  6. OWASP Foundation. (n.d.). SQL Injection Prevention Cheat Sheet. https://cheatsheetseries.owasp.org/cheatsheets/SQL_Injection_Prevention_Cheat_Sheet.html