Final Project

Build a complete PHP app end-to-end — TaskFlow , a small task manager — across six numbered milestones: a database and PDO connection, an MVC router, secure auth, validated CRUD, a JSON API, and a deploy checklist. By the end you'll have wired together everything this course taught into one portfolio-ready application.

Learn Python, JavaScript, Java and more with free interactive lessons, real projects and AI-powered help. Beginner-friendly.

Part of the free Php course at LearnCodingFast — hands-on lessons with examples you run in your browser, plus practice exercises and a quick quiz.

What You'll Build

1️⃣ Schema & PDO Connection

Every app starts with its data . You define the tables once, then open one database connection that the whole app shares. PDO (PHP Data Objects) is PHP's built-in, database-agnostic way to talk to SQL: the same code works against MySQL, PostgreSQL, or SQLite by changing only the connection string. Setting ERRMODE_EXCEPTION means a broken query throws an error you can see, instead of failing silently.

The db() function is the single source of truth for your connection — every later milestone calls it. In production the credentials come from environment variables , never hard-coded into a file you commit.

2️⃣ MVC Structure & Router

MVC (Model–View–Controller) splits your app into three jobs: Models talk to the database, Controllers decide what happens for a request, and Views render the HTML. A front-controller router sends every request through one file ( public/index.php ) that matches the URL to a controller action. Only the public/ folder is web-accessible — your app/ code stays out of reach of the browser.

3️⃣ Authentication (Register & Login)

Now you know who the user is. The golden rule: never store a raw password . password_hash() turns it into a one-way, salted hash; password_verify() checks a login attempt against that hash without ever un-hashing it. Once a login succeeds, you remember the user in a session — a small server-side store keyed to a cookie, so the user stays logged in across requests.

Notice the login is deliberately vague about why it failed (wrong email vs wrong password) — that's intentional, so an attacker can't probe which emails are registered.

4️⃣ CRUD with Prepared Statements & Escaping

CRUD means Create, Read, Update, Delete — the four things every app does to its data. Two rules keep it safe. On the way in: validate input (trim it, reject empties, cap the length) and pass every value as a bound parameter (the ? placeholders) so SQL injection is impossible. On the way out: wrap anything you echo into HTML in htmlspecialchars() so a hostile <script> is shown as text, not executed.

Look at task #2 in the output: the hostile <script> was stored verbatim (the database is just data) but rendered harmless because it was escaped on output. Escape at the boundary where data becomes HTML — that's the whole trick to stopping XSS.

5️⃣ A JSON API Endpoint

A web page is for humans; a JSON API is the same data for machines — your future JavaScript front-end, a mobile app, or another service. The pattern is simple: send a Content-Type: application/json header, set the right HTTP status code, and echo json_encode(...) . Reusing the same query from Milestone 4 means your HTML page and your API never drift apart.

6️⃣ Deploy & Security Checklist

Working on your laptop is not the same as being live. Before you ship, run a checklist — secrets out of the repo, errors hidden from users but logged, HTTPS forced, every query prepared, every output escaped. The script below prints that gate so you can tick it off for real, not from memory.

🎯 Your Turn — Build the Update

Milestone 4 created and read tasks; now add the U in CRUD. The script below is almost done — fill in each ___ using the 👉 hints, then run it and check it against the Output panel. The point: an UPDATE must use bound parameters and a WHERE owner_id = ? so one user can never edit another's task.

🎯 Your Turn — Guard the Private Pages

Auth is only useful if it actually blocks guests. A guard runs before a controller and checks the session. Finish the two ___ below: test the session with isset() , and return the redirect text when no user is present.

📋 Quick Reference — Capstone Building Blocks

No code is filled in this time — just a brief and an outline. Combine three milestones (a model query, a JSON endpoint, and output escaping) into one feature. Build it yourself, run it on onecompiler.com/php or your own machine, then check it against the expected output in the comments. This is exactly how you'll extend a real app.

Practice quiz

In Milestone 1, what does setting PDO::ATTR_ERRMODE to PDO::ERRMODE_EXCEPTION do?

  • Encrypts the database connection
  • Makes queries run faster
  • Turns silent SQL failures into catchable errors
  • Returns rows as numbered arrays

Answer: Turns silent SQL failures into catchable errors. ERRMODE_EXCEPTION makes a broken query throw an error you can see and catch, instead of failing silently.

What is the purpose of the front-controller router (public/index.php) in the MVC milestone?

  • It is the single entry point every request flows through, mapping a URL to a controller action
  • It stores the database password
  • It renders all the HTML templates
  • It runs the password hashing

Answer: It is the single entry point every request flows through, mapping a URL to a controller action. A single front controller boots the app once and dispatches each request to the right controller; only public/ is web-accessible.

In the auth milestone, how is a password stored and later checked?

  • Stored with md5(), checked with ==
  • Stored in plain text, checked with strcmp()
  • Stored with base64_encode(), checked with base64_decode()
  • Stored with password_hash(), checked with password_verify()

Answer: Stored with password_hash(), checked with password_verify(). password_hash() makes a one-way salted hash; password_verify() checks a login attempt against it without un-hashing.

Why does password_hash() not need you to generate your own salt?

  • It doesn't use a salt at all
  • It automatically generates a strong random salt and stores it inside the resulting hash string
  • You must always supply a salt as a second argument
  • It reuses the same salt for every password

Answer: It automatically generates a strong random salt and stores it inside the resulting hash string. password_hash() generates a cryptographically strong random salt and embeds it in the hash, using bcrypt by default.

In the CRUD milestone, how is SQL injection prevented?

  • By passing every value as a bound parameter (the ? placeholders) in a prepared statement
  • By escaping quotes with addslashes()
  • By validating that the input is an integer
  • By using htmlspecialchars() on the SQL

Answer: By passing every value as a bound parameter (the ? placeholders) in a prepared statement. Prepared statements send SQL and values separately, so user input is always treated as data, never as code.

What does htmlspecialchars($value, ENT_QUOTES, 'UTF-8') do, and when is it applied?

  • It hashes the value before storing it
  • It validates input on the way in
  • It escapes characters like < and > on OUTPUT so a stored <script> is shown as text, not executed — stopping XSS
  • It removes the value from the database

Answer: It escapes characters like < and > on OUTPUT so a stored <script> is shown as text, not executed — stopping XSS. Escape on output, every time you echo user-controlled data into HTML; the database stores it verbatim but it renders harmless.

In the JSON API milestone, what is the correct pattern for an endpoint response?

  • echo the raw array directly
  • Send a Content-Type: application/json header, set the HTTP status code, and echo json_encode(...)
  • Return the data as HTML
  • Save the data to a file and redirect

Answer: Send a Content-Type: application/json header, set the HTTP status code, and echo json_encode(...). A JSON endpoint sends the application/json header, the right status code, and echoes json_encode() of the data.

In the 'Your Turn' UPDATE, why does the WHERE clause include owner_id = ?

  • To make the query run faster
  • Because owner_id is the primary key
  • To avoid a syntax error
  • So one user can never edit another user's task

Answer: So one user can never edit another user's task. Adding WHERE owner_id = ? scopes the update to the logged-in user, preventing them from editing other people's tasks.

In the deploy checklist, what should happen to display_errors in production?

  • Set to On so users can report errors
  • Set to Off, with errors logged to a file instead
  • Removed from php.ini entirely
  • Set to a custom error message string

Answer: Set to Off, with errors logged to a file instead. In production, display_errors = Off (don't leak stack traces to users) and log errors to a file instead.

What is the rule of thumb the capstone repeats for handling data safely?

  • Encrypt everything twice
  • Trust the client and validate later
  • Validate on the way in, escape on the way out
  • Escape on input, validate on output

Answer: Validate on the way in, escape on the way out. Validate input at the boundary it enters and escape output at the boundary it becomes HTML — two different jobs, do both.