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:latestDatabase file:
/tmp/lab11.db
Lab Instructions
Step 1: Connect & Create Tables
💡
PDO::ERRMODE_EXCEPTIONmakes PDO throwPDOExceptionon errors — without it, errors returnfalsesilently, which is easy to miss. Always set this in production.PDO::FETCH_ASSOCreturns['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 awhile ($row = $stmt->fetch())loop to process one row at a time.fetchColumn(0)gets just the first column of the first row — ideal forCOUNT(*),MAX(),SUM().
📸 Verified Output:
Step 4: UPDATE & DELETE
💡
rowCount()returns affected rows for INSERT/UPDATE/DELETE — not for SELECT. For SELECT row count, useCOUNT(*)in the query orcount($rows)afterfetchAll(). Always checkrowCount()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_CLASSautomatically maps columns to object properties — no manualnew Product()and assignment needed. If the class has a constructor,PDO::FETCH_CLASS | PDO::FETCH_PROPS_LATEcalls 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
