What is PHP?
PHP (Hypertext Preprocessor) is the server-side language powering over 77% of the web. From WordPress to Laravel to Facebook's early days — PHP is battle-tested, fast, and more modern than you think.
How PHP Works
PHP runs on the server. When a visitor requests a URL, PHP executes on the server, generates HTML, and sends it to the browser. The browser never sees your PHP code — only the result.
Browser → HTTP Request → PHP Server → Executes .php file
↓
Browser ← HTML Response ← PHP generates HTML output
Why PHP in 2026?
JIT Compiler
PHP 8.x has a Just-in-Time compiler making it dramatically faster than PHP 7
Type Safety
Full type system: union types, enums, readonly, intersection types
Ecosystem
Laravel, Symfony, Composer — world-class frameworks & tooling
Jobs
Consistently top 5 most in-demand server-side language globally
Modern Testing
PHPUnit, Pest — first-class testing ecosystem
Easy Deploy
Any shared host, VPS, Docker, or cloud platform supports PHP
PHP Version Timeline
| Version | Key Feature | Status |
|---|---|---|
PHP 8.0 | JIT, Match expression, Named args, Union types | EOL |
PHP 8.1 | Enums, Fibers, Readonly properties, Intersection types | Security only |
PHP 8.2 | Readonly classes, Disjunctive Normal Form types | ✅ Active |
PHP 8.3 | Typed class constants, json_validate(), Override attribute | ✅ Active |
PHP 8.4 | Property Hooks, Asymmetric Visibility, Lazy Objects | ✅ Active |
PHP 8.5 | Pipe Operator |>, URI Extension, Clone With, #[\NoDiscard] | ✅ Latest |
⚡ PHP is Alive — The Numbers Don't Lie
Some people say PHP is dying. The data says the opposite. PHP remains one of the most actively developed, widely deployed, and economically significant languages in the world.
77% of the Web
Over three quarters of all websites with a known server-side language run PHP — including Wikipedia, WordPress, and Facebook's HHVM roots
Annual Releases
PHP ships a major version every November. 8.5 dropped Nov 2025 with the Pipe Operator. PHP 9.0 is actively planned
Laravel is Booming
Laravel is among the most popular web frameworks globally by GitHub stars and job postings in 2026
JIT + 10x Faster
PHP 8.x with OPcache + JIT is 2–3× faster than PHP 7 and 10× faster than PHP 5. Performance is no longer a concern
Modern Type System
Enums, readonly, intersection types, property hooks, generics via Psalm/PHPStan — PHP's type system rivals TypeScript
Rich Ecosystem
Packagist hosts 400,000+ packages. Composer installs have reached billions per month
PHP fundamentals → OOP → PHP 8.4 & 8.5 → Error handling → Namespaces → Magic methods → Generators → Regex → Composer → Testing → MySQL from scratch → JOINs, Indexes, Transactions → Full CRUD with PDO. 42 lessons, zero fluff.
Install PHP 8.5
Get PHP 8.5 — the latest stable release — running on your machine in minutes. We cover macOS, Windows (WSL2), and Docker — the three recommended setups for 2026.
macOS — Laravel Herd (Recommended)
Laravel Herd is the fastest way to get PHP + Nginx on macOS. Download from herd.laravel.com.
# After Herd installs, verify PHP version
php --version
# PHP 8.5.x (cli)
# Switch PHP versions via Herd
herd php85
herd php84
# Start built-in dev server (no Nginx needed for learning)
php -S localhost:8000
Windows — WSL2 + Ubuntu
# Enable WSL2
wsl --install
# Then in Ubuntu terminal:
sudo add-apt-repository ppa:ondrej/php
sudo apt update
sudo apt install php8.5 php8.5-cli php8.5-fpm php8.5-mysql php8.5-curl php8.5-mbstring
php --version
Docker (All Platforms)
services:
app:
image: php:8.5-fpm
volumes: ["./src:/var/www/html"]
ports: ["9000:9000"]
nginx:
image: nginx:alpine
ports: ["8080:80"]
mysql:
image: mysql:8.4
environment: {MYSQL_ROOT_PASSWORD: secret, MYSQL_DATABASE: myapp}
ports: ["3306:3306"]
VS Code + PHP Intelephense extension. It gives autocomplete, type inference, go-to-definition, and error detection in real-time.
Hello World
Write and run your first PHP file. Learn how PHP tags work, how PHP embeds in HTML, and when to use the short echo tag.
<?php
// Output text
echo "Hello, World!";
// Short echo tag — use in HTML templates
// <?= "Hello" ?>
// print() works too (returns 1, slightly slower)
print("Hello!");
// Multiple values
echo "Hello", " ", "World"; // no concatenation needed
PHP Inside HTML
<!DOCTYPE html>
<html>
<body>
<h1><?= "Welcome!" ?></h1>
<p>Today: <?= date('l, F j Y') ?></p>
</body>
</html>
For class and config files (not HTML templates), use <?php at the top and omit the closing ?> tag. This prevents accidental whitespace before headers are sent.
<?php
declare(strict_types=1);
// No closing ?> tag — intentional!
class User
{
public function __construct(
public readonly string $name,
public readonly string $email,
) {}
}
Variables & Types
PHP's type system in 2026 is comprehensive. Learn scalar types, type declarations, union types, intersection types, and why strict_types matters.
<?php
declare(strict_types=1);
// Scalar types
$name = "Alice"; // string
$age = 30; // int
$score = 98.5; // float
$active = true; // bool
$nothing = null; // null
// Inspect types
var_dump($age); // int(30)
gettype($score); // "double"
// Constants
const MAX_RETRIES = 3;
define('APP_ENV', 'production');
// Typed function (PHP 7+ strict types)
function greet(string $name, int $age): string
{
return "Hello {$name}, you are {$age}!";
}
// Union types (PHP 8.0+)
function format(string|int $val): string|float { /*...*/ }
// Nullable type
function find(int $id): ?User { /*...*/ }
// Intersection types (PHP 8.1+) — must implement both
function process(Countable&Iterator $col): void { /*...*/ }
Add declare(strict_types=1); to every PHP file. Without it, PHP silently coerces "3abc" into 3 — hiding bugs that crash in production.
?string mean as a PHP type?Strings
String literals, interpolation, heredoc/nowdoc, and PHP 8's modern string functions like str_contains(), str_starts_with(), and str_ends_with().
$name = "World";
// Double-quoted: interpolates $variables and escape sequences
echo "Hello, {$name}!"; // Hello, World!
echo "Tab:\tNewline:\n";
// Single-quoted: literal string, no interpolation (faster)
echo 'Hello, $name!'; // Hello, $name!
// Heredoc (interpolates variables)
$html = <<<HTML
<p>Hello, {$name}!</p>
HTML;
// PHP 8+ string functions
str_contains("Hello World", "World"); // true
str_starts_with("Hello", "He"); // true
str_ends_with("Hello", "lo"); // true
// Classic functions
strlen("hello"); // 5
strtolower("HELLO"); // hello
strtoupper("hello"); // HELLO
trim(" hello "); // "hello"
str_replace("World", "PHP", "Hello World"); // Hello PHP
explode(",", "a,b,c"); // ["a","b","c"]
implode("-", ["a","b"]); // "a-b"
substr("Hello", 1, 3); // "ell"
sprintf("%.2f", 3.14159); // "3.14"
Arrays
PHP arrays are lists, hashmaps, stacks, and queues all in one. Learn array syntax, destructuring, and the modern functional array functions with arrow functions.
// Indexed array
$fruits = ['apple', 'banana', 'cherry'];
echo $fruits[0]; // apple
// Associative (map/hashmap)
$user = [
'name' => 'Alice',
'email' => '[email protected]',
'age' => 28,
];
// Destructuring (PHP 7.1+)
['name' => $name, 'age' => $age] = $user;
// Spread operator
$merged = [...$fruits, 'date', 'elderberry'];
// Modern functional array functions with arrow functions
$nums = [1, 2, 3, 4, 5, 6];
$doubled = array_map(fn($n) => $n * 2, $nums);
// [2, 4, 6, 8, 10, 12]
$evens = array_filter($nums, fn($n) => $n % 2 === 0);
// [2, 4, 6]
$sum = array_reduce($nums, fn($c, $i) => $c + $i, 0);
// 21
// PHP 8.4: array_find, array_any, array_all
$found = array_find($nums, fn($n) => $n > 4); // 5
$hasLarge = array_any($nums, fn($n) => $n > 5); // true
// Sorting
sort($fruits); // in-place, returns void
$sorted = array_values(array_unique($nums));
// array_column — extract column from 2D array
$users = [['id'=>1,'name'=>'Alice'], ['id'=>2,'name'=>'Bob']];
$names = array_column($users, 'name'); // ['Alice','Bob']
Control Flow
PHP 8's match expression, null coalescing ??, nullsafe ?->, and all the loop types — the complete control flow toolkit.
// if / elseif / else
if ($score >= 90) {
echo "A";
} elseif ($score >= 80) {
echo "B";
} else {
echo "C or below";
}
// match — strict, exhaustive, returns value (PHP 8+)
$label = match($statusCode) {
200 => 'OK',
201 => 'Created',
301, 302 => 'Redirect',
404 => 'Not Found',
default => 'Unknown',
};
// Null coalescing — value or fallback
$name = $_GET['name'] ?? 'Guest';
// Nullsafe operator — short-circuits on null
$city = $user?->getAddress()?->getCity();
// Loops
for ($i = 0; $i < 10; $i++) { /*...*/ }
foreach ($users as $user) {
echo $user['name'];
}
foreach ($map as $key => $value) {
echo "{$key}: {$value}";
}
while ($row = $stmt->fetch()) {
// process DB rows
}
// break and continue work as expected
Functions
Named functions, arrow functions, closures, first-class callables, named arguments, and variadic params — the complete PHP functions guide.
// Typed function with default parameter
function greet(string $name, string $greeting = "Hello"): string
{
return "{$greeting}, {$name}!";
}
// Named arguments (PHP 8.0+) — order doesn't matter
greet(greeting: 'Hi', name: 'Bob'); // Hi, Bob!
// Variadic functions
function sum(int ...$nums): int
{
return array_sum($nums);
}
sum(1, 2, 3, 4); // 10
// Arrow function — captures outer scope automatically
$multiplier = 3;
$triple = fn(int $n): int => $n * $multiplier;
$triple(5); // 15
// Classic closure — must use() to capture
$add = function(int $x) use ($multiplier): int {
return $x + $multiplier;
};
// First-class callables (PHP 8.1+)
$strlen = strlen(...);
array_map($strlen, ['hello', 'world']); // [5, 5]
// never return type — function always throws/exits
function abort(int $code): never
{
throw new RuntimeException("Aborted: {$code}");
}
Classes & Objects
Modern PHP OOP with constructor promotion, readonly properties, and PHP 8.4 property hooks and asymmetric visibility.
<?php
declare(strict_types=1);
class Product
{
// Constructor promotion (PHP 8.0+) — one line per property
public function __construct(
public readonly string $name,
public readonly float $price,
private int $stock = 0,
) {}
// Asymmetric visibility (PHP 8.4)
public private(set) int $views = 0;
// Property hook (PHP 8.4) — computed property
public string $slug {
get => strtolower(str_replace(' ', '-', $this->name));
}
public function isInStock(): bool
{
return $this->stock > 0;
}
public function addStock(int $qty): static
{
$this->stock += $qty;
return $this; // fluent interface
}
}
$p = new Product('Laptop', 999.99);
$p->addStock(10);
echo $p->slug; // "laptop"
echo $p->views; // 0 (readable)
$p->views = 5; // ❌ Error: private(set)
Inheritance & Interfaces
extends, abstract classes, interfaces, traits, and the #[Override] attribute from PHP 8.3 that prevents silent method override bugs.
// Abstract class
abstract class Shape
{
abstract public function area(): float;
public function describe(): string
{
return sprintf("Area: %.2f", $this->area());
}
}
class Circle extends Shape
{
public function __construct(private float $radius) {}
// PHP 8.3: #[Override] — error if parent doesn't have this method
#[Override]
public function area(): float
{
return M_PI * $this->radius ** 2;
}
}
// Interface
interface Serializable
{
public function toJson(): string;
public static function fromJson(string $json): static;
}
// Trait — reusable code snippets
trait Timestampable
{
private ?DateTime $createdAt = null;
public function touch(): void
{
$this->createdAt ??= new DateTime();
}
}
class Post extends BaseModel implements Serializable
{
use Timestampable;
// ...implements interface methods...
}
Match, Fibers & Enums
Three of the most impactful PHP 8.x features: the exhaustive match expression, backed enum types, and PHP Fibers for cooperative concurrency.
// ── ENUMS (PHP 8.1+) ──────────────────────────
// Backed enum — each case has a value
enum Status: string
{
case Active = 'active';
case Inactive = 'inactive';
case Suspended = 'suspended';
public function label(): string
{
return match($this) {
Status::Active => '✓ Active',
Status::Inactive => '○ Inactive',
Status::Suspended => '✕ Suspended',
};
}
}
Status::from('active'); // Status::Active
Status::tryFrom('unknown'); // null (safe)
// Use in type signatures
function activate(Status $s): void { /*...*/ }
// ── READONLY CLASS (PHP 8.2+) ──────────────────
readonly class Money
{
public function __construct(
public int $amount, // in cents
public string $currency,
) {}
public function add(Money $other): static
{
return new static($this->amount + $other->amount, $this->currency);
}
}
// ── FIBERS (PHP 8.1+) — cooperative multitasking ──
$fiber = new Fiber(function(): void {
$val = Fiber::suspend('first yield');
echo "Resumed with: {$val}";
});
$yielded = $fiber->start(); // "first yield"
$fiber->resume('hello'); // "Resumed with: hello"
PHP 8.4 Features
Released November 2024. Property Hooks, Asymmetric Visibility, Lazy Objects, and new array functions — the biggest PHP release since 8.1.
// ── 1. PROPERTY HOOKS ─────────────────────────
class User
{
public string $email {
set(string $value) {
if (!filter_var($value, FILTER_VALIDATE_EMAIL)) {
throw new ValueError("Invalid email");
}
$this->email = strtolower($value);
}
}
public string $displayName {
// Virtual — no backing storage, computed on read
get => ucwords($this->firstName . ' ' . $this->lastName);
}
}
// ── 2. ASYMMETRIC VISIBILITY ──────────────────
class Counter
{
public private(set) int $count = 0;
// Outside: can read. Cannot write. Inside: can do both.
public function increment(): void { $this->count++; }
}
// ── 3. NEW ARRAY FUNCTIONS ────────────────────
$arr = [3, 1, 4, 1, 5, 9, 2, 6];
array_find($arr, fn($v) => $v > 4); // 5
array_find_key($arr, fn($v) => $v > 4); // 4
array_any($arr, fn($v) => $v > 8); // true
array_all($arr, fn($v) => $v > 0); // true
// ── 4. LAZY OBJECTS (Ghost Proxy) ─────────────
$ref = new ReflectionClass(HeavyService::class);
$lazy = $ref->newLazyGhost(function(HeavyService $obj) {
$obj->__construct(); // only called on first property access
});
// ── 5. HTML5 ENCODING (new in 8.4) ────────────
htmlspecialchars($str, ENT_QUOTES | ENT_SUBSTITUTE | ENT_HTML5);
Property hooks are inspired by C# and Kotlin. They allow validation, transformation, and computed properties directly on class properties — no separate getters/setters needed.
PHP 8.5 Features
Released November 20, 2025. The Pipe Operator, URI Extension, Clone With, #[\NoDiscard], and more — a landmark release for developer ergonomics.
1. Pipe Operator |>
The biggest new syntax in 8.5. The pipe operator chains callables left-to-right, eliminating deeply nested function calls. Each callable receives the output of the previous one as its first argument.
// Before PHP 8.5 — deeply nested, read right-to-left
$result = strtolower(
str_replace(['.', '/'], '',
str_replace(' ', '-',
trim($input)
)
)
);
// PHP 8.5 — pipe operator, reads left-to-right
$result = $input
|> trim(...)
|> fn(string $s) => str_replace(' ', '-', $s)
|> fn(string $s) => str_replace(['.', '/'], '', $s)
|> strtolower(...);
// Real-world example: process a request body
$data = file_get_contents('php://input')
|> json_decode(..., true)
|> fn($d) => array_filter($d, 'strlen')
|> array_values(...);
2. URI Extension
PHP 8.5 ships a built-in URI extension for parsing, validating, and manipulating URLs following RFC 3986 and WHATWG URL standards — no more fragile parse_url() arrays. OPcache is now always compiled in.
use Uri\Rfc3986\Uri;
use Uri\WhatWg\Url;
// Before — parse_url() returns a messy array
$parts = parse_url('https://php.net/releases/8.5/en.php');
echo $parts['host']; // "php.net"
// PHP 8.5 — immutable URI object, fluent and typed
$uri = new Uri('https://php.net/releases/8.5/en.php');
$uri->getScheme(); // "https"
$uri->getHost(); // "php.net"
$uri->getPath(); // "/releases/8.5/en.php"
$uri->getQuery(); // null
// Immutable modification — returns new URI
$newUri = $uri->withPath('/downloads')
->withQuery('tab=stable');
// "https://php.net/downloads?tab=stable"
// WHATWG URL standard (browser-compatible parsing)
$url = new Url('https://example.com/path?key=value#section');
$url->getFragment(); // "section"
3. Clone With (Wither Pattern)
Clone an object and override specific properties in a single expression. This was previously a lot of boilerplate with readonly classes — now it's one clean call.
final readonly class Book
{
public function __construct(
public string $title,
public string $author,
public int $year,
) {}
}
$original = new Book('PHP Internals', 'Nikita', 2023);
// Before PHP 8.5 — manual wither methods (lots of boilerplate)
public function withTitle(string $title): static
{
$clone = clone $this;
$clone->title = $title;
return $clone;
}
// PHP 8.5 — clone() with property overrides, zero boilerplate
$updated = clone($original, [
'title' => 'PHP Mastery 2026',
'year' => 2026,
]);
// $updated->author is still 'Nikita'
// $original is unchanged (immutable)
4. #[\NoDiscard] Attribute
Mark functions whose return values must not be ignored. PHP emits a warning if the caller discards the return value — prevents silent bugs like ignoring an error status.
// Mark function — return value must be used
#[\NoDiscard("some items may fail silently")]
function bulkProcess(array $items): array
{
// Returns array of failures — ignoring it hides errors
return processAll($items);
}
bulkProcess($items); // ⚠️ Warning: return value ignored
$failures = bulkProcess($items); // ✅ OK
(void) bulkProcess($items); // ✅ Explicit discard — suppresses warning
// Works on methods too
class Builder
{
#[\NoDiscard]
public function build(): Product
{
return new Product($this->data);
}
}
$builder->build(); // ⚠️ Warning — you forgot to use the result!
$product = $builder->build(); // ✅ Correct
5. New Array Functions
array_first() and array_last() complement the PHP 7.3 key functions — finally a clean way to grab the first or last value of any array without boilerplate.
$users = ['Alice', 'Bob', 'Carol'];
// Before — ugly workarounds
$first = reset($users); // mutates internal pointer
$last = end($users); // mutates internal pointer
$first = $users[array_key_first($users)] ?? null; // verbose
// PHP 8.5 — clean and intention-revealing
$first = array_first($users); // 'Alice'
$last = array_last($users); // 'Carol'
// Works with associative arrays too
$config = ['host' => 'localhost', 'port' => 3306, 'db' => 'myapp'];
array_first($config); // 'localhost'
array_last($config); // 'myapp'
// Returns null for empty arrays (safe!)
array_first([]); // null
6. Closures in Constant Expressions
Static closures and first-class callables can now be used in constant expressions — useful for attribute parameters and compile-time configuration.
// PHP 8.5 — closures valid in constant expressions
const TRANSFORM = static fn(string $s) => strtoupper(trim($s));
const STRLEN = strlen(...);
// Use in attribute parameters
#[Validate(transform: static fn($v) => trim($v))]
class UserDto { /*...*/ }
// Combine with pipe operator
$result = " Hello World "
|> TRANSFORM // "HELLO WORLD"
|> STRLEN; // 11
Released Nov 20, 2025. Active support until Dec 31, 2027. Security fixes until Dec 31, 2029. OPcache is now always compiled in (enablement still controlled by INI). Install via brew install [email protected] on macOS or the official Docker image php:8.5-fpm.
Forms & Validation
Handle HTML forms securely in PHP — reading GET/POST data, validating inputs, and protecting against XSS and CSRF.
<?php
declare(strict_types=1);
// Read POST data safely
$name = trim($_POST['name'] ?? '');
$email = trim($_POST['email'] ?? '');
$age = filter_input(INPUT_POST, 'age', FILTER_VALIDATE_INT);
$errors = [];
// Validate
if (strlen($name) < 2) {
$errors[] = 'Name must be at least 2 characters';
}
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
$errors[] = 'Invalid email address';
}
if ($age === false || $age < 0) {
$errors[] = 'Age must be a positive number';
}
// CSRF protection
if (!hash_equals($_SESSION['csrf'], $_POST['csrf'] ?? '')) {
http_response_code(403);
die('Invalid CSRF token');
}
if (empty($errors)) {
// Safe to process
// Always escape output!
echo htmlspecialchars($name, ENT_QUOTES, 'UTF-8');
}
Sessions & Cookies
PHP sessions for user state, secure cookie configuration, and the recommended session settings for 2026 production apps.
// Secure session config (set before session_start)
ini_set('session.cookie_httponly', '1'); // no JS access
ini_set('session.cookie_secure', '1'); // HTTPS only
ini_set('session.cookie_samesite', 'Lax'); // CSRF protection
ini_set('session.use_strict_mode', '1'); // reject unknown IDs
session_start();
// Store user in session after login
$_SESSION['user_id'] = 42;
$_SESSION['logged_in'] = true;
$_SESSION['csrf'] = bin2hex(random_bytes(32));
// Read session
if ($_SESSION['logged_in'] ?? false) {
echo "Welcome back!";
}
// Regenerate ID on privilege change (prevents session fixation)
session_regenerate_id(true);
// Destroy session on logout
session_unset();
session_destroy();
// Secure cookies
setcookie('remember_token', $token, [
'expires' => time() + 30 * 24 * 3600,
'path' => '/',
'secure' => true,
'httponly' => true,
'samesite' => 'Lax',
]);
JSON & REST APIs
Build and consume REST APIs with PHP — json_encode/decode, php84's json_validate(), HTTP clients, and serving JSON responses.
// Build a simple JSON API endpoint
header('Content-Type: application/json');
header('Access-Control-Allow-Origin: *');
function respond(mixed $data, int $code = 200): never
{
http_response_code($code);
echo json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
exit;
}
// Parse incoming JSON body
$body = file_get_contents('php://input');
// PHP 8.3: json_validate() — check without decoding
if (!json_validate($body)) {
respond(['error' => 'Invalid JSON'], 400);
}
$data = json_decode($body, true, 512, JSON_THROW_ON_ERROR);
// Consume an external API with cURL
$ch = curl_init('https://api.example.com/users');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $token],
CURLOPT_TIMEOUT => 10,
]);
$response = curl_exec($ch);
$users = json_decode($response, true);
respond(['users' => $users, 'total' => count($users)]);
Security Best Practices
The essential PHP security checklist for 2026 — XSS, SQL injection, CSRF, password hashing, and secure headers.
// ── XSS: Always escape output ────────────────
function e(string $s): string
{
return htmlspecialchars($s, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
}
echo e($userInput); // safe!
// ── Passwords: Argon2id ───────────────────────
$hash = password_hash($password, PASSWORD_ARGON2ID);
password_verify($input, $hash); // bool
password_needs_rehash($hash, PASSWORD_ARGON2ID); // bool
// ── Secure random ─────────────────────────────
$token = bin2hex(random_bytes(32)); // 64-char hex token
$otp = random_int(100000, 999999); // 6-digit OTP
// ── Secure HTTP headers ───────────────────────
header('X-Content-Type-Options: nosniff');
header('X-Frame-Options: DENY');
header('Content-Security-Policy: default-src \'self\'');
header('Referrer-Policy: strict-origin-when-cross-origin');
// ── Rate limiting (simple in-memory example) ──
session_start();
$attempts = $_SESSION['login_attempts'] ?? 0;
if ($attempts >= 5) {
http_response_code(429);
die('Too many attempts. Try again later.');
}
✅ strict_types=1 everywhere · ✅ Prepared statements for all SQL · ✅ htmlspecialchars() all output · ✅ Argon2id for passwords · ✅ CSRF tokens on all forms · ✅ Secure session config · ✅ HTTPS everywhere · ✅ Security headers
Numbers & Math
Integers, floats, arbitrary precision with BCMath, and PHP's extensive math function library — everything you need for numeric work.
// Integer literals
$dec = 255;
$hex = 0xFF; // 255
$oct = 0377; // 255
$bin = 0b11111111; // 255
$big = 1_000_000; // underscore separator (PHP 7.4+)
// Float
$pi = 3.14159;
$sci = 1.5e3; // 1500.0
PHP_INT_MAX; // 9223372036854775807 on 64-bit
PHP_FLOAT_EPSILON; // smallest distinguishable float
// Math functions
abs(-42); // 42
round(3.567, 2); // 3.57
ceil(4.1); // 5
floor(4.9); // 4
max(1, 5, 3); // 5
min(1, 5, 3); // 1
pow(2, 10); // 1024
sqrt(144); // 12.0
intdiv(7, 2); // 3 (integer division)
fmod(7.5, 2.5); // 0.0
random_int(1, 100); // cryptographically secure
// Arbitrary precision — for money, never use float!
bcadd('0.1', '0.2', 10); // "0.3000000000"
bcmul('12.50', '100', 2); // "1250.00"
bcdiv('10', '3', 4); // "3.3333"
// Number formatting
number_format(1234567.891, 2, '.', ','); // "1,234,567.89"
Operators
All PHP operators: arithmetic, string, comparison (== vs ===), logical, bitwise, spaceship <=>, and the spread ... operator.
// Arithmetic
10 + 3 // 13
10 - 3 // 7
10 * 3 // 30
10 / 3 // 3.333...
10 % 3 // 1 (modulo)
2 ** 8 // 256 (exponentiation)
// String
"Hello" . " World" // "Hello World" (concatenation)
$s .= "!"; // append
// Comparison — ALWAYS prefer strict === and !==
0 == "foo" // true ⚠️ loose — dangerous!
0 === "foo" // false ✅ strict
1 != "1" // false (loose)
1 !== "1" // true (strict)
// Spaceship — returns -1, 0, or 1
1 <=> 2 // -1 (left smaller)
2 <=> 2 // 0 (equal)
3 <=> 2 // 1 (left larger)
// Useful for usort:
usort($items, fn($a, $b) => $a->price <=> $b->price);
// Logical
true && false // false (and)
true || false // true (or)
!true // false (not)
// Null coalescing assignment
$config['debug'] ??= false;
// Spread operator
function sum(int ...$ns): int { return array_sum($ns); }
sum(...[1,2,3]); // 6 — unpack array as args
PHP's loose comparison == does type coercion with surprising results. Always use strict === unless you explicitly need type juggling. With strict_types=1, function arguments won't coerce — but comparisons still can.
Date & Time
PHP's DateTime, DateTimeImmutable, DateInterval, and DateTimeZone — the right way to handle dates in 2026 without timestamp arithmetic bugs.
// Always use DateTimeImmutable (mutations return new object)
$now = new DateTimeImmutable();
$date = new DateTimeImmutable('2026-03-15 14:30:00');
$utc = new DateTimeImmutable('now', new DateTimeZone('UTC'));
// Format output
$date->format('Y-m-d'); // "2026-03-15"
$date->format('D, d M Y'); // "Sun, 15 Mar 2026"
$date->format('H:i:s'); // "14:30:00"
$date->format('c'); // ISO 8601
$date->format('U'); // Unix timestamp
// Arithmetic — immutable returns new instance
$tomorrow = $date->modify('+1 day');
$nextMonth = $date->modify('+1 month');
$inTwoWeeks = $date->add(new DateInterval('P2W'));
// Difference between two dates
$start = new DateTimeImmutable('2026-01-01');
$end = new DateTimeImmutable('2026-12-31');
$diff = $start->diff($end);
echo $diff->days; // 364
echo $diff->m; // months
// Parse from string
$date = DateTimeImmutable::createFromFormat('d/m/Y', '15/03/2026');
// Timezone conversion
$ny = new DateTimeImmutable('now', new DateTimeZone('America/New_York'));
$utc = $ny->setTimezone(new DateTimeZone('UTC'));
// Legacy date() — still widely used
date('Y-m-d'); // today's date
time(); // Unix timestamp
strtotime('+1 week'); // timestamp for next week
Unlike DateTime, DateTimeImmutable never modifies itself — all methods return a new object. This prevents subtle bugs where passing a date to a function changes it in the caller.
Regular Expressions
PHP uses PCRE (Perl Compatible Regular Expressions). Master preg_match, preg_replace, preg_split, and named capture groups for real-world string parsing.
// preg_match — test if pattern matches
$email = '[email protected]';
if (preg_match('/^[\w.+-]+@[\w-]+\.[a-z]{2,}$/i', $email)) {
echo "Valid email";
}
// Capture groups
preg_match('/(\d{4})-(\d{2})-(\d{2})/', '2026-03-15', $m);
echo $m[1]; // "2026" (year)
echo $m[2]; // "03" (month)
// Named capture groups — much clearer
preg_match('/(?P<year>\d{4})-(?P<month>\d{2})-(?P<day>\d{2})/', '2026-03-15', $m);
echo $m['year']; // "2026"
echo $m['month']; // "03"
// preg_match_all — all occurrences
preg_match_all('/\d+/', 'Order 123, item 456, qty 7', $matches);
// $matches[0] = ["123", "456", "7"]
// preg_replace — replace matches
$safe = preg_replace('/[^a-zA-Z0-9\-]/', '', $input);
$slug = preg_replace('/\s+/', '-', strtolower($title));
// preg_replace_callback — dynamic replacement
$result = preg_replace_callback('/\{(\w+)\}/', function($m) use ($data) {
return $data[$m[1]] ?? $m[0];
}, '{name} is {age} years old');
// preg_split — split by pattern
$words = preg_split('/[\s,;]+/', 'one,two; three four');
// ["one","two","three","four"]
File System
Read, write, and manage files and directories. Covers include/require, file I/O, SPL file classes, and secure file upload handling.
// ── include / require ─────────────────────────
require 'config.php'; // fatal error if missing
require_once 'functions.php'; // only include once
include 'sidebar.php'; // warning only if missing
include_once 'header.php'; // once + warning
// __DIR__ — safe absolute path (never use relative)
require_once __DIR__ . '/vendor/autoload.php';
// ── Read files ────────────────────────────────
$content = file_get_contents('/path/to/file.txt');
$lines = file('/path/to/file.txt', FILE_IGNORE_NEW_LINES);
// ── Write files ───────────────────────────────
file_put_contents('/tmp/log.txt', "Log entry\n", FILE_APPEND);
// ── Low-level file handle ─────────────────────
$fp = fopen('/tmp/data.csv', 'r');
while (($row = fgetcsv($fp)) !== false) {
// process $row array
}
fclose($fp);
// ── Directory operations ──────────────────────
is_file('/path/to/file');
is_dir('/path/to/dir');
file_exists('/path');
mkdir('/tmp/newdir', 0755, true); // recursive
unlink('/tmp/file.txt'); // delete file
rename('/tmp/old', '/tmp/new');
// ── Secure file upload ────────────────────────
if ($_FILES['photo']['error'] === UPLOAD_ERR_OK) {
$info = finfo_file(finfo_open(FILEINFO_MIME_TYPE), $_FILES['photo']['tmp_name']);
$allowed = ['image/jpeg', 'image/png', 'image/webp'];
if (in_array($info, $allowed, true)) {
$dest = __DIR__ . '/uploads/' . bin2hex(random_bytes(16)) . '.jpg';
move_uploaded_file($_FILES['photo']['tmp_name'], $dest);
}
}
Never trust $_FILES['file']['type'] — it comes from the browser and can be spoofed. Always detect the real MIME type server-side using finfo_file(), validate extension, and store files outside the webroot or with randomized names.
Exceptions & Error Handling
try/catch/finally, custom exception classes, multiple catch blocks, the exception hierarchy, and global exception handlers — PHP error handling done right.
// ── Basic try / catch / finally ───────────────
try {
$pdo = new PDO($dsn, $user, $pass);
$result = riskyOperation();
} catch (PDOException $e) {
error_log("DB error: " . $e->getMessage());
throw new RuntimeException("Database unavailable", 0, $e);
} catch (InvalidArgumentException | RangeException $e) {
// catch multiple exception types in one block
echo "Bad input: " . $e->getMessage();
} finally {
// always runs — cleanup resources
closeConnection();
}
// ── Custom exception hierarchy ────────────────
class AppException extends RuntimeException {}
class ValidationException extends AppException
{
public function __construct(
private readonly array $errors,
string $message = 'Validation failed',
) {
parent::__construct($message);
}
public function getErrors(): array { return $this->errors; }
}
// Throw custom exception
if (empty($email)) {
throw new ValidationException(['email' => 'Required']);
}
// ── PHP 8: throw as expression ─────────────────
$user = findUser($id)
?? throw new NotFoundException("User {$id} not found");
// ── Global exception handler ───────────────────
set_exception_handler(function(Throwable $e): never {
error_log($e->getMessage() . "\n" . $e->getTraceAsString());
http_response_code(500);
echo json_encode(['error' => 'Internal Server Error']);
exit(1);
});
// Exception hierarchy cheat sheet:
// Throwable (interface)
// ├── Error (PHP internal errors)
// │ ├── TypeError, ValueError, ParseError...
// └── Exception (user-land)
// ├── RuntimeException
// │ ├── PDOException, OverflowException...
// └── LogicException
// ├── InvalidArgumentException...
Namespaces & Autoloading
Namespaces prevent name collisions and map to directory structure via PSR-4. Composer autoloading means you never manually require a class again.
<?php
declare(strict_types=1);
// Namespace matches directory: App\Models → src/App/Models/
namespace App\Models;
use App\Contracts\HasTimestamps;
use App\Exceptions\ValidationException;
use InvalidArgumentException; // global namespace
class User implements HasTimestamps
{
public function __construct(
public readonly string $email,
) {
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
throw new ValidationException(['email' => 'Invalid']);
}
}
}
// composer.json — PSR-4 autoloading config
{
"autoload": {
"psr-4": {
"App\\": "src/"
}
}
}
// Run: composer dump-autoload
// Then in your entrypoint:
require __DIR__ . '/vendor/autoload.php';
// Now App\Models\User is auto-loaded — no require needed
use App\Models\User;
$user = new User('[email protected]');
// Alias to avoid conflicts
use App\Models\User as UserModel;
use Other\Package\User as OtherUser;
// Namespace separator: backslash \
// Global functions: prepend \ → \strlen(), \array_map()
Magic Methods
PHP's magic methods give classes special behaviours — stringify, invoke as function, intercept undefined calls, clone, and serialize. Essential for elegant API design.
class MagicDemo
{
private array $data = [];
// __toString: object → string representation
public function __toString(): string
{
return json_encode($this->data);
}
// __get / __set: intercept property access
public function __get(string $name): mixed
{
return $this->data[$name] ?? null;
}
public function __set(string $name, mixed $value): void
{
$this->data[$name] = $value;
}
// __isset / __unset
public function __isset(string $name): bool
{
return isset($this->data[$name]);
}
// __call: intercept undefined method calls
public function __call(string $name, array $args): mixed
{
if (str_starts_with($name, 'findBy')) {
$field = lcfirst(substr($name, 6));
return $this->findWhere($field, $args[0]);
}
throw new BadMethodCallException("Method {$name} not found");
}
// __invoke: call object as a function
public function __invoke(string $query): array
{
return $this->search($query);
}
// __clone: run code when object is cloned
public function __clone(): void
{
// deep-clone any nested objects here
$this->data = array_map(fn($v) => is_object($v) ? clone $v : $v, $this->data);
}
}
$obj = new MagicDemo();
$obj->name = 'Alice'; // __set
echo $obj->name; // __get → "Alice"
echo $obj; // __toString
$obj->findByEmail('[email protected]');// __call dynamic finder
$obj('search term'); // __invoke
$clone = clone $obj; // __clone
Static Methods, Properties & Late Static Binding
Static members belong to the class, not instances. Late Static Binding (static::) ensures the correct class is used in inheritance chains — critical for fluent builder patterns.
class Registry
{
// Static property — shared by ALL instances
private static array $instances = [];
// Static method — call on class, not instance
public static function register(string $key, mixed $value): void
{
static::$instances[$key] = $value;
}
public static function get(string $key): mixed
{
return static::$instances[$key] ?? null;
}
}
// Late Static Binding — factory pattern in base class
class Model
{
public static function make(array $attrs): static
{
// static:: resolves to the CALLING class at runtime
// self:: would always resolve to Model
$instance = new static();
foreach ($attrs as $k => $v) { $instance->$k = $v; }
return $instance;
}
public static function className(): string
{
return static::class; // returns child class name
}
}
class Post extends Model {}
$post = Post::make(['title' => 'Hello']); // Post instance ✅
Post::className(); // "Post" — not "Model" ✅
// Singleton pattern
class Config
{
private static ?static $instance = null;
private function __construct() {}
public static function getInstance(): static
{
return static::$instance ??= new static();
}
}
Generators
Generators produce values on-demand with yield — perfect for processing huge datasets without loading everything into memory at once.
// Basic generator — yields values lazily
function fibonacci(): Generator
{
[$a, $b] = [0, 1];
while (true) {
yield $a;
[$a, $b] = [$b, $a + $b];
}
}
$fib = fibonacci();
for ($i = 0; $i < 10; $i++) {
echo $fib->current() . " "; // 0 1 1 2 3 5 8 13 21 34
$fib->next();
}
// Process large CSV without loading it all in memory
function readCsvRows(string $path): Generator
{
$fp = fopen($path, 'r');
$headers = fgetcsv($fp);
while (false !== ($row = fgetcsv($fp))) {
yield array_combine($headers, $row);
}
fclose($fp);
}
foreach (readCsvRows('data.csv') as $row) {
// process one row at a time — constant memory!
}
// yield key => value
function indexedValues(): Generator
{
yield 'first' => 1;
yield 'second' => 2;
yield 'third' => 3;
}
foreach (indexedValues() as $key => $val) {
echo "{$key}: {$val}\n";
}
// yield from — delegate to another generator
function combined(): Generator
{
yield from [1, 2, 3];
yield from fibonacci();
}
A regular function returning array_map over 1 million rows allocates ~100 MB. A generator yields each row one at a time, using only a few KB. Use generators anywhere you'd otherwise return a giant array.
Composer & Packages
Composer is PHP's dependency manager — the single tool that transformed PHP development. Install packages, manage versions, and set up PSR-4 autoloading in minutes.
# Install Composer globally
curl -sS https://getcomposer.org/installer | php
mv composer.phar /usr/local/bin/composer
# Create a new project
composer init
composer require guzzlehttp/guzzle # HTTP client
composer require vlucas/phpdotenv # .env files
composer require --dev pestphp/pest # testing (dev only)
# Update / remove
composer update
composer remove package/name
# Regenerate autoloader
composer dump-autoload --optimize
{
"name": "myapp/api",
"require": {
"php": "^8.5",
"guzzlehttp/guzzle": "^7.0",
"vlucas/phpdotenv": "^5.6"
},
"require-dev": {
"pestphp/pest": "^2.0"
},
"autoload": {
"psr-4": {
"App\\": "src/"
}
},
"scripts": {
"test": "./vendor/bin/pest",
"lint": "./vendor/bin/phpstan analyse"
}
}
// HTTP Client
composer require guzzlehttp/guzzle
composer require symfony/http-client
// Environment variables
composer require vlucas/phpdotenv
// Database / ORM
composer require doctrine/dbal
composer require illuminate/database // Laravel Eloquent standalone
// Template engine
composer require twig/twig
// Static analysis
composer require --dev phpstan/phpstan
// Testing
composer require --dev pestphp/pest
composer require --dev phpunit/phpunit
// Code style
composer require --dev laravel/pint
Testing with Pest
Pest is the modern PHP testing framework — expressive, elegant, and built on top of PHPUnit. Write readable tests that serve as living documentation.
use App\Models\User;
use App\Exceptions\ValidationException;
// Pest — readable, expressive tests
test('creates a valid user', function() {
$user = new User('[email protected]');
expect($user->email)->toBe('[email protected]');
});
test('throws on invalid email', function() {
expect(fn() => new User('not-an-email'))
->toThrow(ValidationException::class);
});
// Describe blocks for grouping
describe('User model', function() {
beforeEach(function() {
$this->user = new User('[email protected]');
});
it('has the correct email', function() {
expect($this->user->email)->toContain('@');
});
});
// Data providers — test multiple cases
test('validates emails', function(string $email, bool $valid) {
$result = filter_var($email, FILTER_VALIDATE_EMAIL) !== false;
expect($result)->toBe($valid);
})->with([
['[email protected]', true],
['not-an-email', false],
['@nodomain.com', false],
]);
# Run tests:
# ./vendor/bin/pest
# ./vendor/bin/pest --coverage
What is MySQL?
MySQL is the world's most popular open-source relational database — the data layer behind WordPress, Shopify, GitHub, and billions of applications.
Relational Databases in 60 Seconds
A relational database stores data in tables (like spreadsheets). Tables have columns (the schema) and rows (the data). Tables can relate to each other via foreign keys.
| id | name | created_at | |
|---|---|---|---|
| 1 | Alice | [email protected] | 2026-01-01 |
| 2 | Bob | [email protected] | 2026-01-02 |
| 3 | Carol | [email protected] | 2026-01-15 |
Blazing Fast
InnoDB engine handles millions of rows with millisecond queries
ACID Transactions
Data integrity guaranteed even during crashes and failures
PHP Native
PHP PDO + MySQL is the classic web stack with deep integration
Free & Open
MySQL Community Edition is completely free and open-source
JSON Support
MySQL 8+ has native JSON column type and JSON functions
Everywhere
Available on every cloud, VPS, and shared hosting provider
MariaDB is a community fork of MySQL with identical SQL syntax. All queries in this course work on both. Many Linux distros default to MariaDB — they're interchangeable for learning.
Install & Connect
Get MySQL 8.4 running locally with the best tools for 2026 — from Homebrew to Docker to TablePlus.
macOS (Homebrew)
brew install mysql
brew services start mysql
mysql_secure_installation
mysql -u root -p
Ubuntu / Debian
sudo apt update && sudo apt install mysql-server
sudo systemctl start mysql
sudo mysql_secure_installation
sudo mysql -u root -p
Docker
services:
mysql:
image: mysql:8.4
environment:
MYSQL_ROOT_PASSWORD: secret
MYSQL_DATABASE: myapp
MYSQL_USER: appuser
MYSQL_PASSWORD: apppass
ports: ["3306:3306"]
volumes: ["mysql_data:/var/lib/mysql"]
volumes:
mysql_data:
Recommended GUI Tools
| Tool | Platform | Best For |
|---|---|---|
| TablePlus | Mac/Win/Linux | Best UI in 2026, free tier |
| phpMyAdmin | Web browser | Quick access, bundled with XAMPP |
| DBeaver | All platforms | Free, supports all databases |
| MySQL Workbench | All platforms | Official Oracle tool |
Databases & Tables
Create and manage databases and tables — the fundamental building blocks. Covers CREATE, ALTER, DROP, and the InnoDB engine.
-- Create database with UTF8MB4 (supports emoji)
CREATE DATABASE myapp
CHARACTER SET utf8mb4
COLLATE utf8mb4_unicode_ci;
USE myapp;
-- Create a complete users table
CREATE TABLE users (
id INT UNSIGNED NOT NULL AUTO_INCREMENT,
name VARCHAR(100) NOT NULL,
email VARCHAR(255) NOT NULL,
password VARCHAR(255) NOT NULL,
role ENUM('user', 'admin') NOT NULL DEFAULT 'user',
is_active TINYINT(1) NOT NULL DEFAULT 1,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (id),
UNIQUE KEY uk_email (email)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- Modify existing table
ALTER TABLE users ADD COLUMN bio TEXT NULL AFTER email;
ALTER TABLE users MODIFY COLUMN name VARCHAR(200) NOT NULL;
ALTER TABLE users DROP COLUMN bio;
-- Inspect
DESCRIBE users;
SHOW CREATE TABLE users;
SHOW TABLES;
The older utf8 charset in MySQL is actually 3-byte and cannot store emoji or many Unicode characters. Always use utf8mb4 for new tables.
MySQL Data Types
Choosing the right type affects storage size, query performance, and data integrity. Here's the complete 2026 guide to MySQL types.
Numeric Types
| Type | Storage | Range | Use For |
|---|---|---|---|
TINYINT | 1 byte | -128 to 127 | Boolean flags (0/1), tiny counts |
INT | 4 bytes | ±2.1 billion | IDs, general counts |
BIGINT | 8 bytes | ±9.2 quintillion | Large IDs, Unix timestamps |
DECIMAL(p,s) | Variable | Exact | Money & finance — always use this |
FLOAT / DOUBLE | 4/8 bytes | Approximate | Scientific data only — not money! |
String Types
| Type | Max | Use For |
|---|---|---|
CHAR(n) | 255 chars | Fixed-length: country codes, hashes |
VARCHAR(n) | 65,535 chars | Variable: names, emails, titles |
TEXT | 65 KB | Short articles, descriptions |
MEDIUMTEXT | 16 MB | Blog posts, large content |
JSON | 4 GB | Semi-structured data (MySQL 8+) |
Date & Time
CREATE TABLE events (
event_date DATE, -- '2026-01-15'
start_time TIME, -- '14:30:00'
scheduled DATETIME, -- '2026-01-15 14:30:00' no TZ
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
-- ↑ auto-converts to UTC, changes on updates
price DECIMAL(10,2), -- 99999999.99 max
metadata JSON -- {"key":"value"} native JSON
);
Binary floating point causes rounding errors: 0.1 + 0.2 ≠ 0.3 in binary. Always use DECIMAL(10,2) for currency.
SELECT Queries
The SELECT statement is the most important in SQL. Master filtering with WHERE, sorting with ORDER BY, pagination with LIMIT/OFFSET, and powerful pattern matching.
-- Basic SELECT
SELECT id, name, email FROM users;
SELECT name AS full_name, email AS contact FROM users;
-- WHERE with operators
SELECT * FROM users
WHERE is_active = 1
AND role = 'admin';
-- Comparison, BETWEEN, IN, LIKE
WHERE age BETWEEN 18 AND 65
WHERE role IN ('admin', 'moderator')
WHERE email LIKE '%@gmail.com'
WHERE deleted_at IS NULL
-- ORDER BY + LIMIT (pagination)
SELECT * FROM users
ORDER BY created_at DESC, name ASC
LIMIT 10 OFFSET 20; -- page 3 (10/page)
-- Subquery
SELECT * FROM users
WHERE id IN (
SELECT user_id FROM orders
WHERE total > 1000
);
-- Full-text search (requires FULLTEXT index)
WHERE MATCH(title, body) AGAINST('mysql tutorial' IN BOOLEAN MODE);
INSERT / UPDATE / DELETE
Write data to MySQL — inserting single and bulk rows, upserts, updating records, soft deletes, and safety rules that protect your data.
-- INSERT single row
INSERT INTO users (name, email, password)
VALUES ('Alice', '[email protected]', 'hashed');
LAST_INSERT_ID(); -- get new auto-increment ID
-- Bulk INSERT (much faster than single inserts)
INSERT INTO users (name, email, password) VALUES
('Bob', '[email protected]', 'hash2'),
('Carol', '[email protected]', 'hash3'),
('Dave', '[email protected]', 'hash4');
-- Upsert: insert or update on duplicate key
INSERT INTO user_stats (user_id, login_count)
VALUES (42, 1)
ON DUPLICATE KEY UPDATE login_count = login_count + 1;
-- UPDATE (ALWAYS include WHERE!)
UPDATE users
SET name = 'Alice Smith',
updated_at = NOW()
WHERE id = 1;
-- Soft delete (preferred — keeps data history)
UPDATE users
SET deleted_at = NOW()
WHERE id = 42;
-- Hard delete
DELETE FROM users WHERE id = 42;
-- TRUNCATE: delete all rows, keep structure
TRUNCATE TABLE temp_logs;
Omitting WHERE from an UPDATE or DELETE affects every row in the table. Always test with a SELECT using the same WHERE clause first.
JOINs
JOINs combine data from multiple tables. They are the heart of relational databases — master JOINs and you master SQL.
| Type | Returns |
|---|---|
INNER JOIN | Only rows that match in both tables |
LEFT JOIN | All rows from left + matching from right (NULL if no match) |
RIGHT JOIN | All rows from right + matching from left |
-- INNER JOIN: posts with their author (no orphans)
SELECT
p.id, p.title,
u.name AS author
FROM posts p
INNER JOIN users u ON p.user_id = u.id;
-- LEFT JOIN: ALL users, even those with no posts
SELECT
u.name,
COUNT(p.id) AS post_count
FROM users u
LEFT JOIN posts p ON p.user_id = u.id
GROUP BY u.id, u.name;
-- Multi-table JOIN
SELECT
p.title,
u.name AS author,
c.name AS category
FROM posts p
INNER JOIN users u ON p.user_id = u.id
INNER JOIN categories c ON p.category_id = c.id
WHERE p.published_at IS NOT NULL
ORDER BY p.published_at DESC
LIMIT 10;
-- Self JOIN: employees and their managers
SELECT
e.name AS employee,
m.name AS manager
FROM employees e
LEFT JOIN employees m ON e.manager_id = m.id;
Aggregate & GROUP BY
COUNT, SUM, AVG, MIN, MAX — turn raw rows into analytics. Combine with GROUP BY and HAVING to build dashboards and reports.
-- Aggregate functions
SELECT
COUNT(*) AS total_users,
COUNT(deleted_at) AS deleted, -- NULLs excluded!
MIN(created_at) AS first_signup,
MAX(created_at) AS latest_signup
FROM users;
-- SUM + AVG for orders
SELECT
SUM(amount) AS total_revenue,
AVG(amount) AS avg_order_value,
MAX(amount) AS largest_order
FROM orders
WHERE status = 'completed';
-- GROUP BY: stats per category
SELECT
role,
COUNT(*) AS count
FROM users
GROUP BY role
ORDER BY count DESC;
-- HAVING: filter groups (WHERE works on rows, HAVING on groups)
SELECT
user_id,
COUNT(*) AS orders,
SUM(amount) AS total_spent
FROM orders
GROUP BY user_id
HAVING total_spent > 1000
ORDER BY total_spent DESC;
-- Monthly signups report
SELECT
DATE_FORMAT(created_at, '%Y-%m') AS month,
COUNT(*) AS signups
FROM users
GROUP BY month
ORDER BY month DESC;
WHERE filters rows before grouping. HAVING filters groups after GROUP BY. You can use both in the same query for maximum power.
Indexes & Performance
Indexes are the biggest lever for MySQL performance. A single index can turn a 10-second query into 1 millisecond.
Without an index, MySQL does a full table scan — reading every row. With an index (a B-tree structure), it jumps directly to matching rows. Add indexes on columns you filter or sort by.
-- Single column index
CREATE INDEX idx_users_email ON users(email);
-- Unique index (also enforces uniqueness)
CREATE UNIQUE INDEX uk_email ON users(email);
-- Composite index — column order matters!
-- Good for: WHERE user_id = ? AND date > ?
CREATE INDEX idx_posts_user_date ON posts(user_id, published_at);
-- Full-text index for MATCH...AGAINST
CREATE FULLTEXT INDEX ft_posts ON posts(title, body);
-- Inspect indexes
SHOW INDEX FROM users;
-- EXPLAIN: see if your query uses indexes
EXPLAIN SELECT * FROM users WHERE email = '[email protected]';
-- Look for: type=ref/eq_ref (good), type=ALL (bad = full scan)
-- key column shows which index MySQL chose
-- EXPLAIN ANALYZE (MySQL 8.0.18+) — actual execution stats
EXPLAIN ANALYZE SELECT * FROM users WHERE email = '[email protected]';
Index columns used in WHERE, JOIN ON, and ORDER BY. Don't over-index — each index slows down writes. For composite indexes, put equality columns first, range columns last.
Relationships & Foreign Keys
Design normalized databases. One-to-many, many-to-many, and one-to-one relationships with enforced referential integrity.
| Type | Example | Implementation |
|---|---|---|
| One-to-Many | User → Posts | FK in child table |
| Many-to-Many | Posts ↔ Tags | Pivot/junction table |
| One-to-One | User → Profile | FK with UNIQUE constraint |
-- One-to-Many: User has many Posts
CREATE TABLE posts (
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
user_id INT UNSIGNED NOT NULL,
title VARCHAR(255) NOT NULL,
FOREIGN KEY (user_id)
REFERENCES users(id)
ON DELETE CASCADE -- posts deleted with user
ON UPDATE CASCADE
) ENGINE=InnoDB;
-- Many-to-Many: Posts ↔ Tags (pivot table)
CREATE TABLE post_tag (
post_id INT UNSIGNED NOT NULL,
tag_id INT UNSIGNED NOT NULL,
PRIMARY KEY (post_id, tag_id),
FOREIGN KEY (post_id) REFERENCES posts(id) ON DELETE CASCADE,
FOREIGN KEY (tag_id) REFERENCES tags(id) ON DELETE CASCADE
) ENGINE=InnoDB;
-- Query M2M: posts with comma-separated tags
SELECT
p.title,
GROUP_CONCAT(t.name ORDER BY t.name SEPARATOR ', ') AS tags
FROM posts p
LEFT JOIN post_tag pt ON p.id = pt.post_id
LEFT JOIN tags t ON pt.tag_id = t.id
GROUP BY p.id;
Transactions & ACID
Transactions group SQL statements into atomic units — all succeed or all fail. Essential for financial operations, inventory management, and any multi-step operation.
Atomic
All statements succeed or all are rolled back — no partial updates
Consistent
Database moves from one valid state to another valid state
Isolated
Concurrent transactions don't see each other's uncommitted changes
Durable
Once committed, data survives crashes and power failures
-- Bank transfer: debit one, credit another
START TRANSACTION;
UPDATE accounts SET balance = balance - 500 WHERE id = 1;
UPDATE accounts SET balance = balance + 500 WHERE id = 2;
COMMIT; -- persist both changes atomically
ROLLBACK; -- undo everything since START TRANSACTION
-- SAVEPOINTs for partial rollbacks
START TRANSACTION;
INSERT INTO orders (...) VALUES (...);
SAVEPOINT after_order;
INSERT INTO order_items (...) VALUES (...);
ROLLBACK TO after_order; -- undo items only
COMMIT; -- keep the order
PDO & Prepared Statements
PHP Data Objects (PDO) is the modern database interface. Prepared statements are the only safe way to use user data in SQL queries.
<?php
declare(strict_types=1);
// Create PDO connection
$pdo = new PDO(
'mysql:host=localhost;dbname=myapp;charset=utf8mb4',
'user', 'password',
[
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false,
]
);
// SELECT with prepared statement
$stmt = $pdo->prepare(
'SELECT id, name, email FROM users WHERE id = :id AND is_active = 1'
);
$stmt->execute(['id' => 42]);
$user = $stmt->fetch(); // one row as array
$users = $stmt->fetchAll(); // all rows
// INSERT
$stmt = $pdo->prepare(
'INSERT INTO users (name, email, password) VALUES (:name, :email, :pw)'
);
$stmt->execute([
'name' => 'Alice',
'email' => '[email protected]',
'pw' => password_hash($password, PASSWORD_ARGON2ID),
]);
$newId = (int) $pdo->lastInsertId();
// Transaction in PHP
$pdo->beginTransaction();
try {
$pdo->prepare('UPDATE accounts SET balance = balance - :amt WHERE id = :id')
->execute(['amt' => 500, 'id' => 1]);
$pdo->prepare('UPDATE accounts SET balance = balance + :amt WHERE id = :id')
->execute(['amt' => 500, 'id' => 2]);
$pdo->commit();
} catch (Throwable $e) {
$pdo->rollBack();
throw $e;
}
NEVER concatenate user input into SQL: "SELECT * FROM users WHERE email = '$email'" is vulnerable. Always use prepared statements with :named placeholders.
Full CRUD Application
Put everything together — a complete, production-ready Repository pattern with typed classes, PHP 8.4, and MySQL. This is real-world PHP + MySQL.
<?php
declare(strict_types=1);
// Value object — readonly class (PHP 8.2+)
readonly class User
{
public function __construct(
public int $id,
public string $name,
public string $email,
public string $createdAt,
) {}
}
class UserRepository
{
public function __construct(
private readonly PDO $pdo
) {}
/** @return User[] */
public function findAll(int $limit = 20, int $offset = 0): array
{
$stmt = $this->pdo->prepare('
SELECT id, name, email, created_at FROM users
WHERE deleted_at IS NULL
ORDER BY created_at DESC
LIMIT :limit OFFSET :offset
');
$stmt->bindValue(':limit', $limit, PDO::PARAM_INT);
$stmt->bindValue(':offset', $offset, PDO::PARAM_INT);
$stmt->execute();
return array_map(fn($r) => new User(...$r), $stmt->fetchAll());
}
public function findById(int $id): ?User
{
$stmt = $this->pdo->prepare('
SELECT id, name, email, created_at FROM users
WHERE id = :id AND deleted_at IS NULL
');
$stmt->execute(['id' => $id]);
$row = $stmt->fetch();
return $row ? new User(...$row) : null;
}
public function create(string $name, string $email, string $password): int
{
$stmt = $this->pdo->prepare('
INSERT INTO users (name, email, password) VALUES (:name, :email, :password)
');
$stmt->execute([
'name' => $name,
'email' => $email,
'password' => password_hash($password, PASSWORD_ARGON2ID),
]);
return (int) $this->pdo->lastInsertId();
}
public function update(int $id, string $name): bool
{
$stmt = $this->pdo->prepare('UPDATE users SET name = :name WHERE id = :id');
$stmt->execute(['id' => $id, 'name' => $name]);
return $stmt->rowCount() > 0;
}
public function softDelete(int $id): bool
{
$stmt = $this->pdo->prepare('UPDATE users SET deleted_at = NOW() WHERE id = :id');
$stmt->execute(['id' => $id]);
return $stmt->rowCount() > 0;
}
}
// Usage (wiring)
$repo = new UserRepository($pdo);
$users = $repo->findAll(limit: 10); // named args
$user = $repo->findById(1);
$id = $repo->create('Alice', '[email protected]', 'secret');
You've finished the full PHP + MySQL course! You know PHP 8.4 with modern OOP, and MySQL from schema design to complex joins, indexes, and transactions. Next step: try Laravel — it wraps all of this in an elegant Eloquent ORM and Query Builder.