🐘 Senior PHP Instructor & Backend Mentor

PHP Mastery
from Zero to
Production

A comprehensive, project-based backend engineering course. Learn PHP the way professional engineering teams actually build applications — with real-world scenarios, security best practices, and modern patterns.

Beginner Friendly
Project-Based
Production Ready
§01

Introduction to PHP

Beginner

PHP (Hypertext Preprocessor) is a server-side scripting language designed specifically for web development. Unlike HTML and CSS which run in the browser, PHP runs on the web server before any content is sent to the user.

How PHP Works — The Request Lifecycle

BrowserHTTP RequestWeb Server (Apache/Nginx)PHP Engine processes .php fileExecutes logic, queries databaseGenerates HTML outputBrowser renders page

Setting Up Your Development Environment

The easiest way to run PHP locally is with an all-in-one stack. XAMPP is cross-platform (Windows, macOS, Linux) and includes Apache, MySQL, and PHP.

💡
Stack Options

XAMPP — Cross-platform, easiest for beginners. WAMP — Windows only. LAMP — Linux native stack for production-like environments. Laravel Herd / Valet — Modern macOS option for advanced users.

  1. 1Download XAMPP from apachefriends.org and run the installer for your OS.
  2. 2Start the XAMPP Control Panel and click Start for both Apache and MySQL.
  3. 3Navigate to your document root: C:\xampp\htdocs\ (Windows) or /Applications/XAMPP/htdocs/ (macOS).
  4. 4Create a folder for your project inside htdocs.
  5. 5Open a browser and visit http://localhost/your-project/

Your First PHP Script

php — hello.php
<?php
// This is a single-line comment
/*
 * This is a multi-line comment block
 * PHP tags: opening <?php and closing ?>
 */

// echo outputs content to the browser
echo "Hello, World!";

// PHP can be embedded directly inside HTML
?>
<!-- This is HTML output after PHP processes -->
<h1>Welcome to PHP</h1>

<?php
// phpinfo() shows your PHP installation details
// Use it to verify your setup — never in production!
phpinfo();
?>

PHP + HTML Integration

php — dynamic-page.php
<?php
// Server-side logic runs first
$name    = "Sarah";
$role    = "Backend Developer";
$year    = date("Y");
$isAdmin = true;
?>
<!DOCTYPE html>
<html lang="en">
<head>
    <title>PHP Dynamic Page</title>
</head>
<body>
    <h1>Hello, <?php echo htmlspecialchars($name); ?>!</h1>
    <p>Role: <?= htmlspecialchars($role) ?></p>

    <?php if ($isAdmin): ?>
        <div class="admin-panel">Admin Panel Visible</div>
    <?php endif; ?>

    <footer>&copy; <?= $year ?></footer>
</body>
</html>
🔐
Best Practice from Day One

Always wrap user-facing output in htmlspecialchars(). This prevents Cross-Site Scripting (XSS) attacks. The shorthand <?= is equivalent to <?php echo.

§02

PHP Basics — The Foundation

Beginner

Variables and Data Types

PHP is dynamically typed — variables don't need explicit type declarations. All variables start with a $ sign.

php — variables.php
<?php
// ── Data Types ──────────────────────────────────────────

$name       = "Alice";           // string
$age        = 28;               // integer
$salary     = 4500.75;          // float / double
$isActive   = true;             // boolean
$nothing    = null;             // null
$tags       = ["php", "web"];  // array

// Type checking
var_dump($age);          // int(28)
var_dump($salary);       // float(4500.75)
gettype($name);          // "string"
is_int($age);            // true
is_null($nothing);       // true

// ── String Operations ────────────────────────────────────
$firstName = "John";
$lastName  = "Doe";

// Concatenation with dot operator
$fullName  = $firstName . " " . $lastName;

// String interpolation (double quotes only)
$greeting  = "Hello, $firstName! You are $age years old.";

// Useful string functions
strlen($fullName);              // 8
strtoupper($fullName);          // "JOHN DOE"
strtolower($fullName);          // "john doe"
str_replace("Doe", "Smith", $fullName); // "John Smith"
trim("  hello  ");               // "hello"
substr($fullName, 0, 4);         // "John"
strpos($fullName, "Doe");        // 5 (position)
?>

Operators

CategoryOperatorsExampleResult
Arithmetic+ - * / % **10 % 31
Comparison== === != !== < > <= >="5" === 5false (strict)
Logical&& || ! and or$a && $bboth true
Assignment= += -= .=$x .= "!"appends
Null Coalescing??$x ?? "default"fallback if null
Spaceship<=>$a <=> $b-1, 0, or 1
⚠️
== vs === — Critical Difference

Always prefer === (strict equality). == "0" and == false and == null can all return true with loose comparison, leading to hard-to-find bugs.

Control Structures

php — control-flow.php
<?php
// ── if / elseif / else ──────────────────────────────────
$score = 78;

if ($score >= 90) {
    echo "Grade: A";
} elseif ($score >= 75) {
    echo "Grade: B";    // this executes
} elseif ($score >= 60) {
    echo "Grade: C";
} else {
    echo "Grade: F";
}

// Ternary shorthand
$status = ($score >= 60) ? "Passed" : "Failed";

// Null coalescing for defaults
$username = $_GET['user'] ?? "Guest";

// ── switch ──────────────────────────────────────────────
$day = "Monday";

switch ($day) {
    case "Monday":
    case "Tuesday":
        echo "Early week";
        break;
    case "Friday":
        echo "Almost weekend!";
        break;
    default:
        echo "Mid-week";
}

// match expression (PHP 8+ — strict, no type coercion)
$grade = match(true) {
    $score >= 90 => "A",
    $score >= 75 => "B",
    $score >= 60 => "C",
    default     => "F",
};

// ── Loops ───────────────────────────────────────────────
// for loop — classic counter
for ($i = 1; $i <= 5; $i++) {
    echo "Item $i<br>";
}

// while loop — unknown iteration count
$count = 0;
while ($count < 3) {
    echo "Count: $count<br>";
    $count++;
}

// foreach — iterate arrays (most common in PHP)
$fruits = ["apple", "banana", "mango"];
foreach ($fruits as $index => $fruit) {
    echo "$index: $fruit<br>";
}
?>

Functions

php — functions.php
<?php
// ── User-Defined Functions ──────────────────────────────

// Basic function with type declarations (PHP 7+ best practice)
function calculateTax(float $price, float $rate = 0.2): float
{
    return $price * $rate;
}

echo calculateTax(100);         // 20.0 (default rate)
echo calculateTax(100, 0.15);  // 15.0

// Returning multiple values via array
function getPriceDetails(float $price): array
{
    $tax   = $price * 0.2;
    $total = $price + $tax;
    return ['price' => $price, 'tax' => $tax, 'total' => $total];
}

['price' => $p, 'total' => $t] = getPriceDetails(50);

// Arrow functions (PHP 7.4+) — concise closures
$double = fn(int $n) => $n * 2;
echo $double(5);  // 10

// Anonymous function / closure
$greet = function(string $name): string {
    return "Hello, $name!";
};

// Variadic function — accepts any number of arguments
function sum(int ...$numbers): int
{
    return array_sum($numbers);
}

echo sum(1, 2, 3, 4, 5);  // 15
?>
§03

Forms & User Input

Beginner

Forms are the primary way users interact with your PHP applications. Handling user input securely is one of the most critical skills in backend development.

Handling GET and POST

php — contact-form.php
<?php
// ── Superglobals ─────────────────────────────────────────
// $_GET    — data from URL query string: ?name=Alice
// $_POST   — data from form body (preferred for sensitive data)
// $_SERVER — server/request metadata
// $_REQUEST — combines GET + POST (avoid in production)

$errors  = [];
$success = false;

// Only process when form is submitted via POST
if ($_SERVER['REQUEST_METHOD'] === 'POST') {

    // 1. Retrieve raw input
    $rawName    = $_POST['name']    ?? '';
    $rawEmail   = $_POST['email']   ?? '';
    $rawMessage = $_POST['message'] ?? '';

    // 2. Sanitize input — remove harmful characters
    $name    = htmlspecialchars(trim($rawName),    ENT_QUOTES, 'UTF-8');
    $email   = filter_var(trim($rawEmail),   FILTER_SANITIZE_EMAIL);
    $message = htmlspecialchars(trim($rawMessage), ENT_QUOTES, 'UTF-8');

    // 3. Validate — check rules and collect errors
    if (empty($name)) {
        $errors['name'] = "Name is required.";
    } elseif (strlen($name) < 2) {
        $errors['name'] = "Name must be at least 2 characters.";
    }

    if (empty($email)) {
        $errors['email'] = "Email is required.";
    } elseif (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
        $errors['email'] = "Please enter a valid email address.";
    }

    if (strlen($message) < 10) {
        $errors['message'] = "Message must be at least 10 characters.";
    }

    // 4. Process only if no validation errors
    if (empty($errors)) {
        // In production: send email, save to DB, etc.
        $success = true;
    }
}
?>

<?php if ($success): ?>
    <div class="alert-success">Message sent! We'll be in touch.</div>
<?php endif; ?>

<form method="POST" action="">
    <input type="text" name="name"
           value="<?= htmlspecialchars($name ?? '') ?>">
    <?php if (isset($errors['name'])): ?>
        <span class="error"><?= $errors['name'] ?></span>
    <?php endif; ?>

    <input type="email" name="email">
    <textarea name="message"></textarea>

    <!-- CSRF token — always protect forms -->
    <input type="hidden" name="csrf_token"
           value="<?= $_SESSION['csrf_token'] ?>">
    <button type="submit">Send Message</button>
</form>
🚨
Never Trust User Input

Every piece of data from $_GET, $_POST, or $_COOKIE is untrusted by default. Always sanitize before display, validate before processing, and use prepared statements before storing in a database.

filter_var() — Your Best Friend

FilterPurpose
FILTER_VALIDATE_EMAILChecks if valid email format
FILTER_VALIDATE_INTValidates integer
FILTER_VALIDATE_URLValidates URL
FILTER_SANITIZE_EMAILRemoves illegal email characters
FILTER_SANITIZE_NUMBER_INTStrips all non-numeric characters
§04

Arrays & Data Handling

Intermediate

Arrays are the workhorse of PHP. You'll use them constantly to store collections of data, map database results, and pass structured data between functions.

php — arrays.php
<?php
// ── Indexed Arrays ───────────────────────────────────────
$colors = ["red", "green", "blue"];
echo $colors[0];  // "red"

$colors[] = "yellow";  // append
array_push($colors, "purple");

// ── Associative Arrays ───────────────────────────────────
$user = [
    'id'       => 42,
    'name'     => 'Alice Johnson',
    'email'    => 'alice@example.com',
    'isAdmin'  => false,
    'scores'   => [95, 87, 92],      // nested array
];

echo $user['name'];              // Alice Johnson
echo $user['scores'][0];        // 95

// ── Multidimensional Arrays ──────────────────────────────
$products = [
    ['id' => 1, 'name' => 'Laptop',  'price' => 999.99],
    ['id' => 2, 'name' => 'Mouse',   'price' => 29.99],
    ['id' => 3, 'name' => 'Monitor', 'price' => 349.00],
];

foreach ($products as $product) {
    echo "{$product['name']}: \${$product['price']}<br>";
}

// ── Essential Array Functions ────────────────────────────
$nums = [3, 1, 4, 1, 5, 9, 2];

sort($nums);                        // sorts in-place: [1,1,2,3,4,5,9]
array_unique($nums);               // remove duplicates
array_reverse($nums);              // reverse order
in_array(5, $nums);               // true
count($nums);                       // 7
array_slice($nums, 0, 3);          // first 3 elements
array_merge($nums, [10, 11]);      // merge arrays
array_keys($user);                 // ['id','name','email',...]
array_values($user);               // [42,'Alice Johnson',...]
array_column($products, 'name');  // ['Laptop','Mouse','Monitor']

// ── Functional Array Methods ─────────────────────────────
// array_map — transform each element
$prices = array_map(fn($p) => $p['price'], $products);

// array_filter — keep elements matching condition
$expensive = array_filter($products, fn($p) => $p['price'] > 100);

// array_reduce — reduce to single value
$total = array_reduce($products, fn($carry, $p) => $carry + $p['price'], 0);

// usort — sort by custom criteria
usort($products, fn($a, $b) => $a['price'] <=> $b['price']);
?>
§05

Sessions & Cookies

Intermediate

HTTP is stateless — each request is independent. Sessions and cookies allow you to persist data across requests, which is essential for user authentication and shopping carts.

Browser  →  login request  →  PHP creates session
                                      ↓ stores in server memory/files
Browser  ←  Set-Cookie: PHPSESSID=abc123  ←  response

Browser  →  next request + Cookie: PHPSESSID=abc123PHP
                                      ↓ looks up session data
                                  User is authenticated!
php — sessions.php
<?php
// MUST call session_start() before any output
session_start();

// ── Setting session data ─────────────────────────────────
$_SESSION['user_id']   = 42;
$_SESSION['username']  = 'alice';
$_SESSION['role']      = 'admin';
$_SESSION['logged_in'] = true;

// ── Reading session data ─────────────────────────────────
if (isset($_SESSION['logged_in']) && $_SESSION['logged_in'] === true) {
    echo "Welcome, {$_SESSION['username']}!";
} else {
    header('Location: login.php');
    exit;  // always exit after header redirect!
}

// ── Auth guard function ──────────────────────────────────
function requireAuth(string $requiredRole = 'user'): void
{
    session_start();
    if (empty($_SESSION['logged_in'])) {
        header('Location: /login.php');
        exit;
    }
    if ($requiredRole === 'admin' && $_SESSION['role'] !== 'admin') {
        header('HTTP/1.1 403 Forbidden');
        exit('Access denied.');
    }
}

// Usage: protect any page with one line
requireAuth('admin');

// ── Logout — properly destroy session ───────────────────
function logout(): void
{
    session_start();
    $_SESSION = [];          // clear all session data
    session_destroy();       // destroy session file
    setcookie(session_name(), '', time() - 3600, '/'); // expire cookie
    header('Location: /login.php');
    exit;
}

// ── Cookies ──────────────────────────────────────────────
// Set a cookie: name, value, expiry, path, domain, secure, httponly
setcookie(
    'remember_token',
    $tokenValue,
    [
        'expires'  => time() + (86400 * 30),  // 30 days
        'path'     => '/',
        'secure'   => true,  // HTTPS only
        'httponly' => true,  // No JavaScript access
        'samesite' => 'Strict'
    ]
);

// Read a cookie
$token = $_COOKIE['remember_token'] ?? null;
?>
⚠️
Session Security

Always call session_regenerate_id(true) after login to prevent session fixation attacks. Set session.cookie_httponly = 1 and session.cookie_secure = 1 in php.ini for production.

§06

File Handling

Intermediate

PHP has powerful built-in functions for reading, writing, and managing files. File uploads require careful validation to prevent security vulnerabilities.

php — file-operations.php
<?php
// ── Reading Files ────────────────────────────────────────
// Read entire file as string
$content = file_get_contents('data/config.json');
$config  = json_decode($content, true);

// Read file as array of lines
$lines = file('data/users.txt', FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);

// ── Writing Files ────────────────────────────────────────
$data = json_encode(['key' => 'value'], JSON_PRETTY_PRINT);
file_put_contents('data/output.json', $data);

// Append to file (logging)
$logEntry = date('Y-m-d H:i:s') . " — User logged in\n";
file_put_contents('logs/app.log', $logEntry, FILE_APPEND | LOCK_EX);

// ── File Upload Handler ──────────────────────────────────
function handleFileUpload(array $file, string $uploadDir): array
{
    // Allowed MIME types (verified server-side, not just extension)
    $allowedTypes = ['image/jpeg', 'image/png', 'image/webp'];
    $maxSize      = 2 * 1024 * 1024;  // 2MB

    // 1. Check for upload errors
    if ($file['error'] !== UPLOAD_ERR_OK) {
        return ['success' => false, 'error' => 'Upload error.'];
    }

    // 2. Validate file size
    if ($file['size'] > $maxSize) {
        return ['success' => false, 'error' => 'File too large (max 2MB).'];
    }

    // 3. Verify MIME type using finfo (not $_FILES['type'] — easily faked)
    $finfo    = new finfo(FILEINFO_MIME_TYPE);
    $mimeType = $finfo->file($file['tmp_name']);

    if (!in_array($mimeType, $allowedTypes, true)) {
        return ['success' => false, 'error' => 'Invalid file type.'];
    }

    // 4. Generate safe filename (never trust user-supplied name)
    $ext      = pathinfo($file['name'], PATHINFO_EXTENSION);
    $safeName = bin2hex(random_bytes(16)) . '.' . strtolower($ext);
    $destPath = $uploadDir . $safeName;

    // 5. Move file from temp location
    if (!move_uploaded_file($file['tmp_name'], $destPath)) {
        return ['success' => false, 'error' => 'Could not save file.'];
    }

    return ['success' => true, 'filename' => $safeName];
}

// Usage
if (isset($_FILES['avatar'])) {
    $result = handleFileUpload($_FILES['avatar'], 'uploads/avatars/');
    if ($result['success']) {
        echo "Uploaded: {$result['filename']}";
    }
}
?>
🚨
File Upload Security Rules

Never trust $_FILES['type'] — it's user-controlled. Always use finfo for MIME detection. Never use the original filename. Store uploads outside the web root when possible, or add a .htaccess rule to prevent execution.

§07

PHP & Databases

Intermediate

Databases are the backbone of dynamic applications. PHP integrates with MySQL via PDO (PHP Data Objects) — the recommended approach that supports multiple databases and provides prepared statement protection.

💡
PDO vs MySQLi

Use PDO for new projects. It works with MySQL, PostgreSQL, SQLite, and others. MySQLi only works with MySQL. Both support prepared statements — but PDO's API is cleaner and more consistent.

Database Connection (PDO)

php — Database.php (connection class)
<?php
class Database
{
    private static ?PDO $instance = null;

    // Private constructor — singleton pattern
    private function __construct() {}

    public static function getInstance(): PDO
    {
        if (self::$instance === null) {
            $dsn = "mysql:host=localhost;dbname=myapp;charset=utf8mb4";
            $opts = [
                PDO::ATTR_ERRMODE            => PDO::ERRMODE_EXCEPTION,
                PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
                PDO::ATTR_EMULATE_PREPARES   => false,  // real prepared statements
            ];
            try {
                self::$instance = new PDO(
                    $dsn,
                    getenv('DB_USER'),  // from .env — never hardcode!
                    getenv('DB_PASS'),
                    $opts
                );
            } catch (PDOException $e) {
                // Log error privately, show generic message publicly
                error_log('DB Connection failed: ' . $e->getMessage());
                throw new RuntimeException('Database unavailable.');
            }
        }
        return self::$instance;
    }
}
?>

CRUD Operations with Prepared Statements

php — UserRepository.php (CRUD)
<?php
class UserRepository
{
    public function __construct(private PDO $db) {}

    // ── CREATE ───────────────────────────────────────────────
    public function create(string $name, string $email, string $password): int
    {
        $sql = "INSERT INTO users (name, email, password_hash, created_at)
                VALUES (:name, :email, :hash, NOW())";

        $stmt = $this->db->prepare($sql);
        $stmt->execute([
            'name'  => $name,
            'email' => $email,
            'hash'  => password_hash($password, PASSWORD_BCRYPT),
        ]);

        return (int) $this->db->lastInsertId();
    }

    // ── READ (single user) ───────────────────────────────────
    public function findById(int $id): ?array
    {
        $stmt = $this->db->prepare(
            "SELECT id, name, email, role, created_at FROM users WHERE id = :id"
        );
        $stmt->execute(['id' => $id]);
        $user = $stmt->fetch();
        return $user ?: null;
    }

    // ── READ (list with pagination) ──────────────────────────
    public function getAll(int $page = 1, int $perPage = 10): array
    {
        $offset = ($page - 1) * $perPage;
        $stmt   = $this->db->prepare(
            "SELECT id, name, email, role FROM users
             ORDER BY created_at DESC LIMIT :limit OFFSET :offset"
        );
        $stmt->bindValue(':limit',  $perPage, PDO::PARAM_INT);
        $stmt->bindValue(':offset', $offset,  PDO::PARAM_INT);
        $stmt->execute();
        return $stmt->fetchAll();
    }

    // ── UPDATE ───────────────────────────────────────────────
    public function update(int $id, array $data): bool
    {
        $stmt = $this->db->prepare(
            "UPDATE users SET name = :name, email = :email WHERE id = :id"
        );
        return $stmt->execute([
            'name'  => $data['name'],
            'email' => $data['email'],
            'id'    => $id,
        ]);
    }

    // ── DELETE ───────────────────────────────────────────────
    public function delete(int $id): bool
    {
        $stmt = $this->db->prepare("DELETE FROM users WHERE id = :id");
        return $stmt->execute(['id' => $id]);
    }

    // ── TRANSACTIONS — for multi-step operations ─────────────
    public function transferCredits(int $fromId, int $toId, int $amount): void
    {
        $this->db->beginTransaction();
        try {
            $debit = $this->db->prepare(
                "UPDATE accounts SET balance = balance - :amt WHERE user_id = :id"
            );
            $debit->execute(['amt' => $amount, 'id' => $fromId]);

            $credit = $this->db->prepare(
                "UPDATE accounts SET balance = balance + :amt WHERE user_id = :id"
            );
            $credit->execute(['amt' => $amount, 'id' => $toId]);

            $this->db->commit();  // all or nothing
        } catch (Exception $e) {
            $this->db->rollBack();  // undo everything on failure
            throw $e;
        }
    }
}
?>
🔒
Prepared Statements = SQL Injection Prevention

Never concatenate user input into SQL queries. "SELECT * FROM users WHERE id = " . $_GET['id'] is a catastrophic vulnerability. Always use prepare() + execute() with named parameters.

§08

Object-Oriented PHP

Advanced

OOP allows you to model real-world concepts as reusable, self-contained objects. Modern PHP frameworks are built entirely on OOP principles.

Classes, Constructors & Encapsulation

php — Product.php
<?php
class Product
{
    // Properties — encapsulated with visibility modifiers
    private float $price;
    private int   $stock;

    // PHP 8 — constructor property promotion (clean syntax)
    public function __construct(
        private readonly int    $id,
        private string $name,
        float  $price,
        int    $stock = 0
    ) {
        $this->setPrice($price);
        $this->stock = $stock;
    }

    // Getter
    public function getPrice(): float  { return $this->price; }
    public function getName(): string  { return $this->name; }
    public function isInStock(): bool  { return $this->stock > 0; }

    // Setter with validation — encapsulation protects data integrity
    public function setPrice(float $price): void
    {
        if ($price < 0) {
            throw new InvalidArgumentException("Price cannot be negative.");
        }
        $this->price = $price;
    }

    public function decreaseStock(int $quantity): void
    {
        if ($quantity > $this->stock) {
            throw new RuntimeException("Insufficient stock.");
        }
        $this->stock -= $quantity;
    }

    // Magic method — called when object used as string
    public function __toString(): string
    {
        return "{$this->name} (\${$this->price})";
    }
}

// Usage
$laptop = new Product(1, 'MacBook Pro', 1999.99, 5);
echo $laptop;                  // "MacBook Pro ($1999.99)"
echo $laptop->isInStock();     // true
?>

Inheritance, Interfaces & Traits

php — OOP Advanced Patterns
<?php
// ── Interface — defines a contract ──────────────────────
interface Payable
{
    public function processPayment(float $amount): bool;
    public function getReceiptNumber(): string;
}

// ── Abstract class — shared base with required overrides ─
abstract class Payment
{
    protected string $receiptNumber;

    public function __construct(protected string $currency = 'USD') {
        $this->receiptNumber = strtoupper(bin2hex(random_bytes(6)));
    }

    // Concrete method shared by all children
    public function getReceiptNumber(): string { return $this->receiptNumber; }

    // Abstract — each child MUST implement this
    abstract public function processPayment(float $amount): bool;
}

// ── Trait — reusable code mixin ──────────────────────────
trait Timestampable
{
    private ?DateTime $createdAt = null;
    private ?DateTime $updatedAt = null;

    public function touch(): void
    {
        $now = new DateTime();
        if ($this->createdAt === null) $this->createdAt = $now;
        $this->updatedAt = $now;
    }
}

// ── Concrete classes implement interface + extend abstract ─
class StripePayment extends Payment implements Payable
{
    use Timestampable;  // mixin

    public function processPayment(float $amount): bool
    {
        // Real implementation calls Stripe SDK
        $this->touch();  // from Timestampable trait
        error_log("Stripe: charging {$amount} {$this->currency}");
        return true;
    }
}

class PayPalPayment extends Payment implements Payable
{
    use Timestampable;

    public function processPayment(float $amount): bool
    {
        $this->touch();
        error_log("PayPal: charging {$amount} {$this->currency}");
        return true;
    }
}

// Polymorphism — same interface, different implementations
function checkout(Payable $gateway, float $amount): void
{
    if ($gateway->processPayment($amount)) {
        echo "Receipt: " . $gateway->getReceiptNumber();
    }
}

checkout(new StripePayment(), 99.99);
checkout(new PayPalPayment(), 99.99);
?>
§09

PHP for Web Development

Advanced

Modern PHP web development uses the MVC pattern (Model-View-Controller) to separate concerns. Frameworks like Laravel implement this brilliantly.

MVC Architecture Diagram

HTTP RequestRouterController
                                    │
                      ┌─────────────┼─────────────┐
                      ↓             ↓             ↓
                  Model       Business      View
                (Database)       Logic      (Templates)
                      │
                      └──────→  ControllerView renders HTMLResponse

Simple Router & MVC from Scratch

php — index.php (front controller + router)
<?php
// All requests go through index.php (Apache: mod_rewrite / .htaccess)
// Nginx: try_files $uri /index.php;

spl_autoload_register(function (string $class) {
    require_once __DIR__ . '/src/' . str_replace('\\', '/', $class) . '.php';
});

// Router maps URLs to controller actions
$routes = [
    'GET'  => [
        '/'              => ['HomeController',    'index'],
        '/posts'         => ['PostController',    'index'],
        '/posts/create'  => ['PostController',    'create'],
    ],
    'POST' => [
        '/posts'         => ['PostController',    'store'],
        '/login'         => ['AuthController',    'login'],
    ],
];

$method = $_SERVER['REQUEST_METHOD'];
$uri    = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);

if (isset($routes[$method][$uri])) {
    [$controllerName, $action] = $routes[$method][$uri];
    $controller = new $controllerName();
    $controller->$action();
} else {
    http_response_code(404);
    require 'views/404.php';
}
?>
php — PostController.php (Controller)
<?php
class PostController
{
    private PostRepository $posts;

    public function __construct()
    {
        $db          = Database::getInstance();
        $this->posts = new PostRepository($db);
    }

    // GET /posts — list all posts
    public function index(): void
    {
        $posts = $this->posts->getAll();
        require 'views/posts/index.php';  // $posts available in view
    }

    // POST /posts — create new post
    public function store(): void
    {
        requireAuth();  // protect route

        $title   = htmlspecialchars(trim($_POST['title']   ?? ''));
        $content = htmlspecialchars(trim($_POST['content'] ?? ''));

        if (empty($title) || empty($content)) {
            http_response_code(422);
            require 'views/posts/create.php';
            return;
        }

        $id = $this->posts->create($title, $content, $_SESSION['user_id']);
        header("Location: /posts/{$id}");
        exit;
    }
}
?>

Getting Started with Laravel

🚀
Laravel — The PHP Framework for Web Artisans

Laravel gives you routing, ORM (Eloquent), authentication, queues, caching, testing, and more out of the box. It's the most popular PHP framework and the industry standard for modern applications.

bash — Laravel Quick Start
# Install Laravel via Composer
composer create-project laravel/laravel my-app
cd my-app
php artisan serve     # starts dev server at localhost:8000

# Generate a controller
php artisan make:controller PostController --resource

# Generate a model + migration
php artisan make:model Post -m

# Run migrations
php artisan migrate

# Generate authentication scaffolding
composer require laravel/breeze --dev
php artisan breeze:install
php — Laravel Route + Controller + Eloquent
// routes/web.php
Route::resource('posts', PostController::class);
Route::middleware('auth')->group(function () {
    Route::get('/dashboard', [DashboardController::class, 'index']);
});

// app/Models/Post.php — Eloquent Model
class Post extends Model
{
    use HasFactory;

    protected $fillable = ['title', 'content', 'user_id'];

    // Relationship: post belongs to a user
    public function author(): BelongsTo
    {
        return $this->belongsTo(User::class, 'user_id');
    }
}

// app/Http/Controllers/PostController.php
class PostController extends Controller
{
    public function index(): View
    {
        // Eloquent: paginated posts with author relationship
        $posts = Post::with('author')->latest()->paginate(10);
        return view('posts.index', compact('posts'));
    }

    public function store(StorePostRequest $request): RedirectResponse
    {
        // Form Request handles all validation automatically
        $request->user()->posts()->create($request->validated());
        return redirect()->route('posts.index')->with('success', 'Post created!');
    }
}
§10

Security Best Practices

Advanced

Security is not an afterthought — it must be built into every layer of your application from day one. Here are the most critical vulnerabilities and how to prevent them.

1. SQL Injection Prevention Critical

php — sql-injection.php
<?php
// ❌ VULNERABLE — Never do this!
$id  = $_GET['id'];  // attacker sends: 1 OR 1=1; DROP TABLE users; --
$sql = "SELECT * FROM users WHERE id = $id";

// ✅ SAFE — Always use prepared statements
$stmt = $pdo->prepare("SELECT * FROM users WHERE id = :id");
$stmt->execute(['id' => (int) $_GET['id']]);
?>

2. XSS Prevention Critical

php — xss-prevention.php
<?php
// ❌ VULNERABLE — outputs raw user HTML
echo $_GET['search'];  // attacker: <script>document.cookie</script>

// ✅ SAFE — always escape output
echo htmlspecialchars($_GET['search'], ENT_QUOTES | ENT_HTML5, 'UTF-8');

// For rich text (blog posts etc.) use HTML Purifier library
require_once 'vendor/ezyang/htmlpurifier/library/HTMLPurifier.auto.php';
$purifier = new HTMLPurifier();
$safeHtml = $purifier->purify($userContent);

// Set Content-Security-Policy headers to prevent inline scripts
header("Content-Security-Policy: default-src 'self'; script-src 'self'");
?>

3. Password Hashing Critical

php — passwords.php
<?php
// ❌ NEVER store passwords as plain text or MD5/SHA1
$bad = md5($password);   // easily crackable with rainbow tables

// ✅ ALWAYS use password_hash with PASSWORD_BCRYPT or PASSWORD_ARGON2ID
$hash = password_hash($password, PASSWORD_ARGON2ID, [
    'memory_cost' => 65536,  // 64MB
    'time_cost'   => 4,
    'threads'     => 3,
]);

// ✅ Verify on login
if (password_verify($inputPassword, $storedHash)) {
    // Rehash if algorithm/cost has been upgraded
    if (password_needs_rehash($storedHash, PASSWORD_ARGON2ID)) {
        $newHash = password_hash($inputPassword, PASSWORD_ARGON2ID);
        // Update hash in database
    }
    // Login successful
}

// ✅ CSRF Protection — generate and validate tokens
function generateCsrfToken(): string
{
    if (empty($_SESSION['csrf_token'])) {
        $_SESSION['csrf_token'] = bin2hex(random_bytes(32));
    }
    return $_SESSION['csrf_token'];
}

function validateCsrfToken(string $token): bool
{
    return hash_equals($_SESSION['csrf_token'] ?? '', $token);
    // hash_equals is timing-safe (prevents timing attacks)
}
?>

Security Checklist

  • Use PDO prepared statements for all database queries
  • Escape all output with htmlspecialchars()
  • Hash passwords with password_hash() using bcrypt or Argon2
  • Implement CSRF tokens on all state-changing forms
  • Set HttpOnly, Secure, and SameSite on cookies
  • Regenerate session ID after login
  • Validate file uploads server-side with finfo MIME detection
  • Never display raw database error messages to users
  • Set error_reporting(0) and display_errors=0 in production
  • Use HTTPS everywhere and set Strict-Transport-Security header
§11

Mini Projects — Hands-On

Projects
🟢 Beginner Project

Contact Form with Validation

Build a contact form that validates all fields, prevents XSS, generates CSRF tokens, and sends email via PHP's mail() or PHPMailer.

php — Beginner Project: Contact Form (complete)
<?php
session_start();

// Generate CSRF token
if (empty($_SESSION['csrf'])) {
    $_SESSION['csrf'] = bin2hex(random_bytes(32));
}

$errors  = [];
$success = false;
$old     = [];  // re-populate form fields on error

if ($_SERVER['REQUEST_METHOD'] === 'POST') {

    // Validate CSRF token
    if (!hash_equals($_SESSION['csrf'], $_POST['csrf_token'] ?? '')) {
        die('Invalid token. Please refresh and try again.');
    }

    // Sanitize
    $old['name']    = htmlspecialchars(trim($_POST['name']    ?? ''));
    $old['email']   = filter_var(trim($_POST['email']   ?? ''), FILTER_SANITIZE_EMAIL);
    $old['message'] = htmlspecialchars(trim($_POST['message'] ?? ''));

    // Validate
    if (strlen($old['name']) < 2)
        $errors['name'] = "Name must be at least 2 characters.";

    if (!filter_var($old['email'], FILTER_VALIDATE_EMAIL))
        $errors['email'] = "Valid email required.";

    if (strlen($old['message']) < 20)
        $errors['message'] = "Message must be at least 20 characters.";

    if (empty($errors)) {
        // Send email (use PHPMailer in production for SMTP)
        $to      = 'admin@yoursite.com';
        $subject = "Contact Form: {$old['name']}";
        $body    = "From: {$old['name']} <{$old['email']}>\n\n{$old['message']}";
        $headers = "From: noreply@yoursite.com\r\nReply-To: {$old['email']}";

        if (mail($to, $subject, $body, $headers)) {
            $success = true;
            $_SESSION['csrf'] = bin2hex(random_bytes(32));  // rotate token
            $old = [];
        }
    }
}
?>
🟡 Intermediate Project

User Login & Registration System

Full auth system with registration, email verification concept, secure login, session management, password reset flow, and role-based access control.

php — Intermediate Project: Auth System
<?php
class AuthService
{
    public function __construct(private PDO $db) {}

    // ── Registration ─────────────────────────────────────────
    public function register(string $name, string $email, string $password): array
    {
        // Check if email already exists
        $stmt = $this->db->prepare("SELECT id FROM users WHERE email = :email");
        $stmt->execute(['email' => $email]);
        if ($stmt->fetch()) {
            return ['success' => false, 'error' => 'Email already registered.'];
        }

        // Validate password strength
        if (strlen($password) < 8) {
            return ['success' => false, 'error' => 'Password must be 8+ characters.'];
        }

        // Generate email verification token
        $verifyToken = bin2hex(random_bytes(32));

        $stmt = $this->db->prepare(
            "INSERT INTO users (name, email, password_hash, verify_token, created_at)
             VALUES (:name, :email, :hash, :token, NOW())"
        );
        $stmt->execute([
            'name'  => $name,
            'email' => $email,
            'hash'  => password_hash($password, PASSWORD_BCRYPT, ['cost' => 12]),
            'token' => $verifyToken,
        ]);

        return ['success' => true, 'user_id' => (int) $this->db->lastInsertId()];
    }

    // ── Login ─────────────────────────────────────────────────
    public function login(string $email, string $password): array
    {
        $stmt = $this->db->prepare(
            "SELECT * FROM users WHERE email = :email AND is_active = 1"
        );
        $stmt->execute(['email' => $email]);
        $user = $stmt->fetch();

        // Use consistent response time to prevent timing attacks
        if (!$user || !password_verify($password, $user['password_hash'])) {
            return ['success' => false, 'error' => 'Invalid credentials.'];
        }

        // Prevent session fixation
        session_regenerate_id(true);
        $_SESSION['user_id']   = $user['id'];
        $_SESSION['username']  = $user['name'];
        $_SESSION['role']      = $user['role'];
        $_SESSION['logged_in'] = true;
        $_SESSION['last_seen'] = time();

        return ['success' => true];
    }
}
?>
🔴 Advanced Project

Full CRUD Blog Application

A complete blog backend with post management, category taxonomy, image uploads, user roles (admin/editor/viewer), pagination, search, and a RESTful JSON API.

sql — Blog Database Schema
-- users table
CREATE TABLE users (
    id            INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    name          VARCHAR(100) NOT NULL,
    email         VARCHAR(255) UNIQUE NOT NULL,
    password_hash VARCHAR(255) NOT NULL,
    role          ENUM('admin','editor','viewer') DEFAULT 'viewer',
    is_active     TINYINT(1) DEFAULT 1,
    created_at    DATETIME NOT NULL
);

-- posts table
CREATE TABLE posts (
    id           INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    user_id      INT UNSIGNED NOT NULL,
    title        VARCHAR(255) NOT NULL,
    slug         VARCHAR(255) UNIQUE NOT NULL,
    content      LONGTEXT NOT NULL,
    excerpt      TEXT,
    cover_image  VARCHAR(500),
    status       ENUM('draft','published') DEFAULT 'draft',
    published_at DATETIME,
    created_at   DATETIME NOT NULL,
    FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
    FULLTEXT INDEX idx_search (title, content)
);

-- categories
CREATE TABLE categories (
    id   INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    name VARCHAR(100) UNIQUE NOT NULL,
    slug VARCHAR(100) UNIQUE NOT NULL
);

-- pivot table: many-to-many posts <-> categories
CREATE TABLE post_categories (
    post_id     INT UNSIGNED NOT NULL,
    category_id INT UNSIGNED NOT NULL,
    PRIMARY KEY (post_id, category_id)
);
php — Advanced Project: PostRepository with Search + Pagination
<?php
class PostRepository
{
    public function __construct(private PDO $db) {}

    // Full-text search with pagination
    public function search(string $query, int $page = 1, int $perPage = 10): array
    {
        $offset = ($page - 1) * $perPage;

        $stmt = $this->db->prepare(
            "SELECT p.*, u.name as author_name,
                    MATCH(p.title, p.content) AGAINST(:q) as relevance
             FROM posts p
             JOIN users u ON p.user_id = u.id
             WHERE p.status = 'published'
               AND MATCH(p.title, p.content) AGAINST(:q IN BOOLEAN MODE)
             ORDER BY relevance DESC
             LIMIT :limit OFFSET :offset"
        );
        $stmt->bindValue(':q',      $query);
        $stmt->bindValue(':limit',  $perPage, PDO::PARAM_INT);
        $stmt->bindValue(':offset', $offset,  PDO::PARAM_INT);
        $stmt->execute();

        return $stmt->fetchAll();
    }

    // Generate URL-friendly slug
    public function generateSlug(string $title): string
    {
        $slug = strtolower(trim(preg_replace('/[^A-Za-z0-9-]+/', '-', $title)));
        $base = $slug;
        $i    = 1;

        // Ensure unique slug
        while ($this->slugExists($slug)) {
            $slug = "$base-$i";
            $i++;
        }
        return $slug;
    }

    private function slugExists(string $slug): bool
    {
        $stmt = $this->db->prepare("SELECT 1 FROM posts WHERE slug = :slug");
        $stmt->execute(['slug' => $slug]);
        return (bool) $stmt->fetch();
    }

    // JSON API response helper
    public static function jsonResponse(array $data, int $status = 200): void
    {
        http_response_code($status);
        header('Content-Type: application/json; charset=UTF-8');
        echo json_encode($data, JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR);
    }
}
?>
§12

Performance & Best Practices

Advanced

Error Handling & Logging

php — error-handling.php
<?php
// ── Production Error Configuration ───────────────────────
// In php.ini for production:
// display_errors = Off
// log_errors = On
// error_log = /var/log/php/error.log

// ── Custom Exception Handler ─────────────────────────────
set_exception_handler(function (Throwable $e) {
    error_log(sprintf(
        "[%s] %s in %s:%d\nStack: %s",
        date('Y-m-d H:i:s'),
        $e->getMessage(),
        $e->getFile(),
        $e->getLine(),
        $e->getTraceAsString()
    ));

    http_response_code(500);
    echo json_encode(['error' => 'An unexpected error occurred.']);
    exit;
});

// ── Structured Exception Handling ────────────────────────
try {
    $user = $userRepo->findById($id);
    if (!$user) {
        throw new RuntimeException("User #$id not found.", 404);
    }
} catch (PDOException $e) {
    error_log('Database error: ' . $e->getMessage());
    throw new RuntimeException('Database unavailable.', 503);
} catch (RuntimeException $e) {
    http_response_code($e->getCode() ?: 500);
    echo json_encode(['error' => $e->getMessage()]);
} finally {
    // Always executes — good for cleanup
    $db = null;
}
?>

Performance Techniques

php — caching.php
<?php
// ── Simple File Cache (for small projects) ───────────────
function cacheGet(string $key, int $ttl = 3600): mixed
{
    $file = sys_get_temp_dir() . '/cache_' . md5($key);
    if (file_exists($file) && (time() - filemtime($file)) < $ttl) {
        return unserialize(file_get_contents($file));
    }
    return null;
}

function cacheSet(string $key, mixed $data): void
{
    $file = sys_get_temp_dir() . '/cache_' . md5($key);
    file_put_contents($file, serialize($data), LOCK_EX);
}

// Usage — cache expensive DB query for 1 hour
$cacheKey = 'featured_posts';
$posts    = cacheGet($cacheKey);

if ($posts === null) {
    $posts = $postRepo->getFeatured();
    cacheSet($cacheKey, $posts);
}

// ── APCu Cache (in-memory, fast) ─────────────────────────
if (apcu_exists('site_config')) {
    $config = apcu_fetch('site_config');
} else {
    $config = $db->query("SELECT * FROM settings")->fetchAll();
    apcu_store('site_config', $config, 86400);
}
?>

Deployment Basics

StepActionCommand / Tool
1Dependency installationcomposer install --no-dev --optimize-autoloader
2Environment configSet .env variables — never commit to repo
3Database migrationsphp artisan migrate --force (Laravel)
4Cachingphp artisan config:cache && php artisan route:cache
5Web server configNginx / Apache with PHP-FPM, HTTPS (Let's Encrypt)
6File permissionschmod 755 for dirs, 644 for files, writable storage/
7MonitoringSentry for errors, New Relic / DataDog for performance

Production Readiness Checklist

  • PDO prepared statements for all database queries
  • All output escaped with htmlspecialchars()
  • Passwords hashed with password_hash() bcrypt/argon2
  • CSRF tokens on all state-changing forms
  • File uploads validated via MIME type (not extension)
  • Session security: httponly, secure, samesite cookies
  • display_errors = Off in production php.ini
  • Error logging to file (not browser)
  • Database credentials in environment variables
  • HTTPS enforced with HSTS header
  • Composer autoloading optimized
  • OPcache enabled for bytecode caching