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.
Introduction to PHP
BeginnerPHP (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
Browser → HTTP Request → Web Server (Apache/Nginx) ↓ PHP Engine processes .php file ↓ Executes logic, queries database ↓ Generates HTML output → Browser 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.
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.
- 1Download XAMPP from
apachefriends.organd run the installer for your OS. - 2Start the XAMPP Control Panel and click Start for both Apache and MySQL.
- 3Navigate to your document root:
C:\xampp\htdocs\(Windows) or/Applications/XAMPP/htdocs/(macOS). - 4Create a folder for your project inside
htdocs. - 5Open a browser and visit
http://localhost/your-project/
Your First PHP Script
<?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
// 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>© <?= $year ?></footer>
</body>
</html>
Always wrap user-facing output in htmlspecialchars(). This prevents Cross-Site Scripting (XSS) attacks. The shorthand <?= is equivalent to <?php echo.
PHP Basics — The Foundation
BeginnerVariables and Data Types
PHP is dynamically typed — variables don't need explicit type declarations. All variables start with a $ sign.
<?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
| Category | Operators | Example | Result |
|---|---|---|---|
| Arithmetic | + - * / % ** | 10 % 3 | 1 |
| Comparison | == === != !== < > <= >= | "5" === 5 | false (strict) |
| Logical | && || ! and or | $a && $b | both true |
| Assignment | = += -= .= | $x .= "!" | appends |
| Null Coalescing | ?? | $x ?? "default" | fallback if null |
| Spaceship | <=> | $a <=> $b | -1, 0, or 1 |
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
// ── 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
// ── 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
?>
Forms & User Input
BeginnerForms 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
// ── 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>
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
| Filter | Purpose |
|---|---|
FILTER_VALIDATE_EMAIL | Checks if valid email format |
FILTER_VALIDATE_INT | Validates integer |
FILTER_VALIDATE_URL | Validates URL |
FILTER_SANITIZE_EMAIL | Removes illegal email characters |
FILTER_SANITIZE_NUMBER_INT | Strips all non-numeric characters |
Arrays & Data Handling
IntermediateArrays 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
// ── 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']);
?>
Sessions & Cookies
IntermediateHTTP 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=abc123 → PHP ↓ looks up session data User is authenticated!
<?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;
?>
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.
File Handling
IntermediatePHP has powerful built-in functions for reading, writing, and managing files. File uploads require careful validation to prevent security vulnerabilities.
<?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']}";
}
}
?>
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.
PHP & Databases
IntermediateDatabases 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.
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
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
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;
}
}
}
?>
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.
Object-Oriented PHP
AdvancedOOP 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
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
// ── 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);
?>
PHP for Web Development
AdvancedModern PHP web development uses the MVC pattern (Model-View-Controller) to separate concerns. Frameworks like Laravel implement this brilliantly.
MVC Architecture Diagram
HTTP Request → Router → Controller │ ┌─────────────┼─────────────┐ ↓ ↓ ↓ Model Business View (Database) Logic (Templates) │ └──────→ Controller → View renders HTML → Response
Simple Router & MVC from Scratch
<?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
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 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.
# 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
// 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!');
}
}
Security Best Practices
AdvancedSecurity 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
// ❌ 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
// ❌ 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
// ❌ 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, andSameSiteon 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)anddisplay_errors=0in production - Use HTTPS everywhere and set
Strict-Transport-Securityheader
Mini Projects — Hands-On
ProjectsContact 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
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 = [];
}
}
}
?>
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
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];
}
}
?>
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.
-- 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
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);
}
}
?>
Performance & Best Practices
AdvancedError Handling & Logging
<?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
// ── 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
| Step | Action | Command / Tool |
|---|---|---|
| 1 | Dependency installation | composer install --no-dev --optimize-autoloader |
| 2 | Environment config | Set .env variables — never commit to repo |
| 3 | Database migrations | php artisan migrate --force (Laravel) |
| 4 | Caching | php artisan config:cache && php artisan route:cache |
| 5 | Web server config | Nginx / Apache with PHP-FPM, HTTPS (Let's Encrypt) |
| 6 | File permissions | chmod 755 for dirs, 644 for files, writable storage/ |
| 7 | Monitoring | Sentry 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 = Offin 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