Lab 11: Database with PDO & SQLite

Objective

Connect to SQLite using PHP's PDO (PHP Data Objects), perform CRUD operations with prepared statements, use transactions, and build a simple data access layer.

Background

PDO is PHP's unified database API — the same code works with SQLite, MySQL, PostgreSQL, and 10+ other databases. Using prepared statements prevents SQL injection attacks. SQLite requires no server setup — perfect for learning, embedded apps, and prototypes. Production apps typically use MySQL or PostgreSQL with the same PDO API.

Time

35 minutes

Prerequisites

  • Lab 09 (Error Handling), Lab 10 (File I/O)

Tools

  • PHP 8.3 CLI with PDO + PDO_SQLite extensions

  • Docker image: zchencow/innozverse-php:latest

  • Database file: /tmp/lab11.db


Lab Instructions

Step 1: Connect & Create Tables

💡 PDO::ERRMODE_EXCEPTION makes PDO throw PDOException on errors — without it, errors return false silently, which is easy to miss. Always set this in production. PDO::FETCH_ASSOC returns ['id' => 1, 'name' => '...'] instead of [0 => 1, 'id' => 1, ...] (both numeric and string keys).

📸 Verified Output:


Step 2: INSERT with Prepared Statements

💡 Prepared statements separate SQL structure from data — the database parses the SQL once, then you supply values separately. This prevents SQL injection: if $name = "'; DROP TABLE products; --", the statement treats it as a literal string, not SQL. Never concatenate user input into SQL.

📸 Verified Output:


Step 3: SELECT — Query & Fetch

💡 fetchAll() loads all rows into memory — fine for small result sets. For large results, use a while ($row = $stmt->fetch()) loop to process one row at a time. fetchColumn(0) gets just the first column of the first row — ideal for COUNT(*), MAX(), SUM().

📸 Verified Output:


Step 4: UPDATE & DELETE

💡 rowCount() returns affected rows for INSERT/UPDATE/DELETE — not for SELECT. For SELECT row count, use COUNT(*) in the query or count($rows) after fetchAll(). Always check rowCount() after updates to verify the change happened — a 0 means your WHERE clause matched nothing.

📸 Verified Output:


Step 5: Transactions

💡 Transactions are atomic — either ALL operations succeed (commit) or NONE take effect (rollback). Without a transaction, a crash between the deduct and add steps would leave your inventory inconsistent. Always wrap multi-step data operations in a transaction.

📸 Verified Output:


Step 6: Aggregate Queries & Reporting

📸 Verified Output:


Step 7: PDO Fetch Modes

💡 PDO::FETCH_CLASS automatically maps columns to object properties — no manual new Product() and assignment needed. If the class has a constructor, PDO::FETCH_CLASS | PDO::FETCH_PROPS_LATE calls the constructor first, then sets properties. This is how ORMs like Doctrine hydrate entity objects.

📸 Verified Output:


Step 8: Complete — Repository Pattern

💡 The Repository Pattern abstracts database access — controllers call $repo->findByCategory('Laptop') without knowing SQL. Swap SQLite for MySQL by changing one constructor line. Add caching by wrapping the repository. This pattern is how Laravel's Eloquent and Doctrine repositories work.

📸 Verified Output:


Verification

Summary

PDO is PHP's production database interface. You've performed CRUD with prepared statements, used transactions for atomicity, built aggregate reports, explored fetch modes, and implemented the Repository pattern. These skills directly apply to Laravel Eloquent, Doctrine ORM, and any raw PDO application.

Further Reading

Last updated