PHP powers over 77% of all websites with a known server-side language — including WordPress, Facebook (originally), Wikipedia, and Laravel apps worldwide. This tutorial takes you from zero to building real PHP web applications, with no prior programming experience required.
What you'll learn
| Topic | What you'll be able to do |
|---|---|
| Setup | Install PHP and run your first script |
| Syntax & variables | Understand how PHP code works |
| Data types | Work with strings, numbers, arrays, booleans |
| Control flow | Use if/else, loops, and switch |
| Functions | Write reusable, clean code |
| Arrays | Work with indexed and associative arrays |
| Forms | Handle HTML form data securely |
| OOP | Build classes, objects, and interfaces |
| MySQL/PDO | Connect to a database with prepared statements |
| Composer | Manage packages like a professional |
| Projects | Build 3 real web programs |
PHP version used: PHP 8.3+ (latest stable as of 2025)
Part 1 — Why PHP?
PHP is the dominant language for server-side web development for several reasons:
1. Runs on every web host — shared hosting, VPS, cloud
2. Powers WordPress (43% of all websites)
3. Laravel is one of the best web frameworks ever built
4. Easy to learn: HTML + PHP in the same file to start
5. Huge ecosystem via Composer (Packagist: 400k+ packages)
6. PHP 8.x is modern: named args, attributes, fibers, JIT
PHP vs other languages
| Language | Strength | When to pick |
|---|---|---|
| PHP | Web hosting everywhere, WordPress, Laravel | Web apps, CMSes, APIs, shared hosting |
| Python | AI/ML, data science, general purpose | ML projects, data pipelines, scripting |
| JavaScript (Node.js) | Frontend + backend unified | Full-stack JS apps, real-time |
| Ruby | Rails productivity | Rapid prototyping, startups |
| Go | Performance, concurrency | High-throughput APIs, microservices |
| Java | Enterprise, Android | Large-scale enterprise systems |
Part 2 — Setup
Option A: Local server (recommended)
Windows: Install XAMPP — includes Apache + PHP + MySQL.
Mac: Install Homebrew then:
brew install php
php -v # verify
Linux (Ubuntu/Debian):
sudo apt update
sudo apt install php php-cli php-mysql php-curl php-xml
php -v
Option B: PHP built-in server
mkdir myapp && cd myapp
echo "<?php echo 'Hello World!';" > index.php
php -S localhost:8000 # visit http://localhost:8000
Your first PHP file
Create hello.php:
<?php
echo "Hello, World!";
echo "<br>";
echo "PHP version: " . PHP_VERSION;
?>
Run with php hello.php in terminal, or open in browser via XAMPP.
Key facts about PHP files:
- Must start with
<?php(the opening tag) - Closing tag
?>is optional in pure PHP files (omitting it is best practice) - Statements end with
; - PHP is embedded in HTML — server processes PHP, browser receives HTML
Part 3 — Variables and Data Types
Variables
PHP variables start with $:
<?php
$name = "Alice";
$age = 30;
$height = 5.9;
$isStudent = true;
echo $name; // Alice
echo $age; // 30
var_dump($isStudent); // bool(true)
Rules:
- Start with
$followed by a letter or underscore - Case-sensitive:
$nameand$Nameare different - No type declaration needed (PHP infers the type)
Data types
| Type | Example | Description |
|---|---|---|
string |
"Hello" |
Text in quotes (single or double) |
int |
42 |
Whole numbers |
float |
3.14 |
Decimal numbers |
bool |
true, false |
Boolean |
array |
[1, 2, 3] |
List or map of values |
null |
null |
No value |
object |
new MyClass() |
Instance of a class |
<?php
$str = "Hello";
$int = 42;
$float = 3.14;
$bool = true;
$null = null;
$arr = [1, 2, 3];
echo gettype($str); // string
echo gettype($int); // integer
echo gettype($bool); // boolean
echo gettype($null); // NULL
Type casting
<?php
$strNum = "42";
$num = (int) $strNum; // 42 as integer
$flt = (float) "3.14"; // 3.14 as float
$str = (string) 100; // "100" as string
$bool = (bool) 0; // false (0 is falsy)
String operations
<?php
$first = "John";
$last = "Doe";
// Concatenation
$full = $first . " " . $last; // "John Doe"
// String interpolation (double quotes only)
echo "Hello, $first!"; // Hello, John!
echo "Hello, {$first}!"; // Hello, John! (safe with complex vars)
// Useful string functions
echo strlen("hello"); // 5
echo strtoupper("hello"); // HELLO
echo strtolower("HELLO"); // hello
echo str_replace("o", "0", "hello"); // hell0
echo trim(" hello "); // "hello"
echo substr("hello world", 6); // world
echo str_contains("hello world", "world"); // 1 (true) — PHP 8+
echo str_starts_with("hello", "he"); // 1 — PHP 8+
echo str_ends_with("hello", "lo"); // 1 — PHP 8+
echo strpos("hello world", "world"); // 6
Constants
<?php
define('MAX_USERS', 100);
const DB_NAME = 'myapp';
echo MAX_USERS; // 100
echo DB_NAME; // myapp
// PHP built-in constants
echo PHP_VERSION; // 8.3.x
echo PHP_EOL; // newline character
echo PHP_INT_MAX; // 9223372036854775807
Part 4 — Control Flow
if / elseif / else
<?php
$age = 20;
if ($age >= 18) {
echo "Adult";
} elseif ($age >= 13) {
echo "Teenager";
} else {
echo "Child";
}
Comparison and logical operators
<?php
// Comparison
$a = 5;
$b = "5";
var_dump($a == $b); // true (loose: value only)
var_dump($a === $b); // false (strict: value AND type)
var_dump($a !== $b); // true
var_dump($a > 3); // true
var_dump($a <= 5); // true
// Logical
$x = true;
$y = false;
var_dump($x && $y); // false
var_dump($x || $y); // true
var_dump(!$x); // false
// Spaceship operator (PHP 7+)
echo 1 <=> 2; // -1 (left is less)
echo 2 <=> 2; // 0 (equal)
echo 3 <=> 2; // 1 (left is greater)
switch
<?php
$day = "Monday";
switch ($day) {
case "Saturday":
case "Sunday":
echo "Weekend";
break;
case "Monday":
echo "Start of the week";
break;
default:
echo "Weekday";
}
match (PHP 8+)
<?php
$status = 200;
$text = match($status) {
200 => "OK",
301 => "Moved Permanently",
404 => "Not Found",
500 => "Server Error",
default => "Unknown",
};
echo $text; // OK
match is strict (uses ===), has no fallthrough, and is an expression.
Ternary and null coalescing
<?php
$age = 20;
// Ternary
$label = $age >= 18 ? "adult" : "minor";
// Null coalescing (PHP 7+)
$username = $_GET['user'] ?? 'guest'; // 'guest' if not set or null
// Null coalescing assignment (PHP 7.4+)
$config['debug'] ??= false; // set to false if not already set
Loops
<?php
// for
for ($i = 0; $i < 5; $i++) {
echo $i . " "; // 0 1 2 3 4
}
// while
$count = 0;
while ($count < 3) {
echo $count++;
} // 012
// do-while (runs at least once)
$n = 5;
do {
echo $n;
$n--;
} while ($n > 0);
// foreach (arrays)
$fruits = ["apple", "banana", "cherry"];
foreach ($fruits as $fruit) {
echo $fruit . "\n";
}
// foreach with key
$person = ["name" => "Alice", "age" => 30];
foreach ($person as $key => $value) {
echo "$key: $value\n";
}
// break and continue
for ($i = 0; $i < 10; $i++) {
if ($i === 5) break; // stop loop
if ($i % 2 === 0) continue; // skip even
echo $i . " "; // 1 3
}
Part 5 — Functions
Basic functions
<?php
function greet(string $name): string {
return "Hello, $name!";
}
echo greet("Alice"); // Hello, Alice!
Default parameters
<?php
function createUser(string $name, string $role = "user", int $age = 0): array {
return ['name' => $name, 'role' => $role, 'age' => $age];
}
print_r(createUser("Alice")); // role=user, age=0
print_r(createUser("Bob", "admin", 25));
Named arguments (PHP 8+)
<?php
function makeTag(string $tag, string $content, string $class = ""): string {
$cls = $class ? " class=\"$class\"" : "";
return "<$tag$cls>$content</$tag>";
}
echo makeTag(content: "Hello", tag: "p", class: "intro");
// <p class="intro">Hello</p>
Variadic functions
<?php
function sum(int ...$numbers): int {
return array_sum($numbers);
}
echo sum(1, 2, 3, 4, 5); // 15
Type declarations
<?php
declare(strict_types=1); // Enforce strict types
function divide(float $a, float $b): float {
if ($b === 0.0) {
throw new InvalidArgumentException("Cannot divide by zero");
}
return $a / $b;
}
echo divide(10, 3); // 3.3333...
Anonymous functions and closures
<?php
// Anonymous function
$double = function(int $n): int {
return $n * 2;
};
echo $double(5); // 10
// Arrow function (PHP 7.4+) — automatically captures outer variables
$multiplier = 3;
$multiply = fn(int $n) => $n * $multiplier;
echo $multiply(5); // 15
// Using closures with array functions
$numbers = [1, 2, 3, 4, 5, 6];
$evens = array_filter($numbers, fn($n) => $n % 2 === 0);
$doubled = array_map(fn($n) => $n * 2, $numbers);
$sum = array_reduce($numbers, fn($carry, $n) => $carry + $n, 0);
print_r($evens); // [2, 4, 6]
print_r($doubled); // [2, 4, 6, 8, 10, 12]
echo $sum; // 21
Part 6 — Arrays
PHP arrays are extremely flexible — they act as lists, maps, stacks, and queues.
Indexed arrays
<?php
$fruits = ["apple", "banana", "cherry"];
echo $fruits[0]; // apple
echo count($fruits); // 3
// Add / remove
$fruits[] = "date"; // append
array_push($fruits, "elderberry"); // append (multiple)
$last = array_pop($fruits); // remove last
$first = array_shift($fruits); // remove first
array_unshift($fruits, "avocado"); // prepend
// Iterate
foreach ($fruits as $index => $fruit) {
echo "$index: $fruit\n";
}
Associative arrays (maps)
<?php
$user = [
"name" => "Alice",
"age" => 30,
"email" => "alice@example.com",
];
echo $user["name"]; // Alice
// Add / update / delete
$user["city"] = "London";
$user["age"] = 31;
unset($user["email"]);
// Check existence
var_dump(isset($user["name"])); // true
var_dump(array_key_exists("city", $user)); // true
// Get all keys / values
print_r(array_keys($user)); // ["name", "age", "city"]
print_r(array_values($user)); // ["Alice", 31, "London"]
Array functions
<?php
$numbers = [3, 1, 4, 1, 5, 9, 2, 6];
sort($numbers); // sort ascending (in-place)
rsort($numbers); // sort descending
$sorted = [...$numbers];
sort($sorted); // spread to avoid mutating original
$unique = array_unique([1, 2, 2, 3, 3]); // [1, 2, 3]
$sliced = array_slice($numbers, 1, 3); // 3 items from index 1
$merged = array_merge([1, 2], [3, 4]); // [1, 2, 3, 4]
$flipped = array_flip(["a" => 1, "b" => 2]); // [1 => "a", 2 => "b"]
// Search
$pos = array_search("banana", ["apple", "banana", "cherry"]); // 1
$found = in_array(5, [1, 2, 5, 8]); // true
// Map, filter, reduce
$nums = [1, 2, 3, 4, 5];
$squared = array_map(fn($n) => $n ** 2, $nums); // [1, 4, 9, 16, 25]
$odds = array_filter($nums, fn($n) => $n % 2 !== 0); // [1, 3, 5]
$total = array_reduce($nums, fn($c, $n) => $c + $n, 0); // 15
// Sorting associative arrays
$people = [
["name" => "Charlie", "age" => 30],
["name" => "Alice", "age" => 25],
["name" => "Bob", "age" => 35],
];
usort($people, fn($a, $b) => $a["age"] <=> $b["age"]);
// Sorted by age: Alice, Charlie, Bob
Multidimensional arrays
<?php
$matrix = [
[1, 2, 3],
[4, 5, 6],
[7, 8, 9],
];
echo $matrix[1][2]; // 6
$users = [
["name" => "Alice", "role" => "admin"],
["name" => "Bob", "role" => "user"],
];
foreach ($users as $user) {
echo "{$user['name']} is a {$user['role']}\n";
}
Spread operator and destructuring
<?php
$first = [1, 2, 3];
$second = [4, 5, 6];
$combined = [...$first, ...$second]; // [1, 2, 3, 4, 5, 6]
// Array destructuring
[$a, $b, $c] = [10, 20, 30];
echo $a; // 10
// Skip elements
[, $second, , $fourth] = [1, 2, 3, 4];
// Named destructuring (associative)
["name" => $name, "age" => $age] = ["name" => "Alice", "age" => 30];
Part 7 — Handling Forms (Web)
This is where PHP's web superpowers shine.
HTML form + PHP handler
<!-- form.html -->
<form method="POST" action="process.php">
<input type="text" name="username" placeholder="Username" required>
<input type="email" name="email" placeholder="Email" required>
<input type="password" name="password" placeholder="Password" required>
<button type="submit">Register</button>
</form>
<?php
// process.php
declare(strict_types=1);
// Only process POST requests
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405);
die("Method Not Allowed");
}
// Sanitize and validate input
$username = trim($_POST['username'] ?? '');
$email = trim($_POST['email'] ?? '');
$password = $_POST['password'] ?? '';
$errors = [];
if (strlen($username) < 3) {
$errors[] = "Username must be at least 3 characters";
}
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
$errors[] = "Invalid email address";
}
if (strlen($password) < 8) {
$errors[] = "Password must be at least 8 characters";
}
if (!empty($errors)) {
foreach ($errors as $error) {
echo htmlspecialchars($error) . "<br>";
}
exit;
}
// Hash password before storing
$hashedPassword = password_hash($password, PASSWORD_BCRYPT);
echo "User registered: " . htmlspecialchars($username);
Superglobals cheat sheet
| Superglobal | Contains |
|---|---|
$_GET |
URL query parameters (?name=value) |
$_POST |
POST form data |
$_REQUEST |
Both GET and POST |
$_FILES |
Uploaded file data |
$_SESSION |
Session variables (per user) |
$_COOKIE |
Browser cookies |
$_SERVER |
Server info (headers, paths, methods) |
$_ENV |
Environment variables |
$GLOBALS |
All global variables |
NEVER trust user input
<?php
// WRONG — XSS vulnerability
echo $_GET['name'];
// RIGHT — escape output
echo htmlspecialchars($_GET['name'] ?? '', ENT_QUOTES, 'UTF-8');
// WRONG — SQL injection
$sql = "SELECT * FROM users WHERE name = '{$_GET['name']}'";
// RIGHT — prepared statements (see Part 9)
$stmt = $pdo->prepare("SELECT * FROM users WHERE name = ?");
$stmt->execute([$_GET['name']]);
Part 8 — Object-Oriented Programming
Classes and objects
<?php
class Animal {
// Properties
protected string $name;
protected string $sound;
// Constructor
public function __construct(string $name, string $sound) {
$this->name = $name;
$this->sound = $sound;
}
// Methods
public function speak(): string {
return "{$this->name} says {$this->sound}!";
}
public function getName(): string {
return $this->name;
}
}
$dog = new Animal("Rex", "Woof");
echo $dog->speak(); // Rex says Woof!
Constructor property promotion (PHP 8+)
<?php
class Product {
public function __construct(
private readonly string $name,
private float $price,
private int $stock = 0,
) {}
public function getPrice(): float { return $this->price; }
public function getName(): string { return $this->name; }
public function isAvailable(): bool { return $this->stock > 0; }
public function applyDiscount(float $percent): static {
$this->price *= (1 - $percent / 100);
return $this; // method chaining
}
}
$product = new Product("Laptop", 999.99, 5);
echo $product->applyDiscount(10)->getPrice(); // 899.991
Inheritance
<?php
class Shape {
public function __construct(protected string $color = 'red') {}
public function area(): float {
return 0.0;
}
public function describe(): string {
return "A {$this->color} shape with area " . $this->area();
}
}
class Circle extends Shape {
public function __construct(
private float $radius,
string $color = 'red'
) {
parent::__construct($color);
}
public function area(): float {
return M_PI * $this->radius ** 2;
}
}
class Rectangle extends Shape {
public function __construct(
private float $width,
private float $height,
string $color = 'blue'
) {
parent::__construct($color);
}
public function area(): float {
return $this->width * $this->height;
}
}
$shapes = [
new Circle(5, 'red'),
new Rectangle(4, 6, 'blue'),
];
foreach ($shapes as $shape) {
echo $shape->describe() . "\n";
}
Interfaces
<?php
interface Cacheable {
public function getCacheKey(): string;
public function ttl(): int;
}
interface Serializable {
public function toArray(): array;
public static function fromArray(array $data): static;
}
class UserProfile implements Cacheable, Serializable {
public function __construct(
private int $id,
private string $name,
private string $email,
) {}
public function getCacheKey(): string {
return "user:{$this->id}";
}
public function ttl(): int {
return 3600; // 1 hour
}
public function toArray(): array {
return ['id' => $this->id, 'name' => $this->name, 'email' => $this->email];
}
public static function fromArray(array $data): static {
return new static($data['id'], $data['name'], $data['email']);
}
}
Abstract classes
<?php
abstract class PaymentGateway {
abstract public function charge(float $amount, string $currency): bool;
abstract public function refund(string $transactionId): bool;
// Shared concrete method
protected function formatAmount(float $amount): string {
return number_format($amount, 2);
}
public function processPayment(float $amount, string $currency): string {
$success = $this->charge($amount, $currency);
return $success
? "Charged " . $this->formatAmount($amount) . " $currency"
: "Payment failed";
}
}
class StripeGateway extends PaymentGateway {
public function charge(float $amount, string $currency): bool {
// Stripe API call here
return true;
}
public function refund(string $transactionId): bool {
// Stripe refund here
return true;
}
}
Abstract class vs Interface
| Abstract class | Interface | |
|---|---|---|
| Can have concrete methods | Yes | Only default methods (PHP 8+) |
| Can have properties | Yes | No |
| Constructor | Yes | No |
| Multiple inheritance | No (single) | Yes (multiple) |
| When to use | Share code between related classes | Define a contract for unrelated classes |
Static members
<?php
class Counter {
private static int $count = 0;
public static function increment(): void {
self::$count++;
}
public static function getCount(): int {
return self::$count;
}
public static function reset(): void {
self::$count = 0;
}
}
Counter::increment();
Counter::increment();
Counter::increment();
echo Counter::getCount(); // 3
Traits
<?php
trait Timestamps {
private ?DateTime $createdAt = null;
private ?DateTime $updatedAt = null;
public function setCreatedAt(): void {
$this->createdAt = new DateTime();
}
public function touch(): void {
$this->updatedAt = new DateTime();
}
public function getCreatedAt(): ?DateTime {
return $this->createdAt;
}
}
trait SoftDelete {
private ?DateTime $deletedAt = null;
public function delete(): void {
$this->deletedAt = new DateTime();
}
public function isDeleted(): bool {
return $this->deletedAt !== null;
}
}
class Post {
use Timestamps, SoftDelete;
public function __construct(private string $title) {
$this->setCreatedAt();
}
}
$post = new Post("Hello World");
$post->delete();
var_dump($post->isDeleted()); // true
Enums (PHP 8.1+)
<?php
enum Status: string {
case Active = 'active';
case Inactive = 'inactive';
case Pending = 'pending';
public function label(): string {
return match($this) {
Status::Active => 'Active',
Status::Inactive => 'Inactive',
Status::Pending => 'Pending Review',
};
}
}
$status = Status::Active;
echo $status->value; // active
echo $status->label(); // Active
// From value
$fromDb = Status::from('pending');
echo $fromDb->label(); // Pending Review
Part 9 — MySQL with PDO
PDO (PHP Data Objects) lets you connect to MySQL, PostgreSQL, SQLite, and more with a unified API and prepared statements.
Connect to MySQL
<?php
declare(strict_types=1);
$host = 'localhost';
$dbname = 'myapp';
$user = 'root';
$pass = 'secret';
$port = '3306';
try {
$pdo = new PDO(
"mysql:host=$host;port=$port;dbname=$dbname;charset=utf8mb4",
$user,
$pass,
[
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false,
]
);
} catch (PDOException $e) {
// Never echo $e->getMessage() in production (leaks credentials)
error_log($e->getMessage());
die("Database connection failed");
}
CRUD with prepared statements
<?php
// CREATE TABLE
$pdo->exec("
CREATE TABLE IF NOT EXISTS users (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(100) NOT NULL,
email VARCHAR(200) NOT NULL UNIQUE,
password VARCHAR(255) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
");
// INSERT (positional params)
$stmt = $pdo->prepare(
"INSERT INTO users (name, email, password) VALUES (?, ?, ?)"
);
$stmt->execute(["Alice", "alice@example.com", password_hash("secret", PASSWORD_BCRYPT)]);
$newId = (int) $pdo->lastInsertId();
// INSERT (named params)
$stmt = $pdo->prepare(
"INSERT INTO users (name, email, password) VALUES (:name, :email, :pass)"
);
$stmt->execute([
':name' => 'Bob',
':email' => 'bob@example.com',
':pass' => password_hash("hunter2", PASSWORD_BCRYPT),
]);
// SELECT all
$stmt = $pdo->query("SELECT id, name, email FROM users");
$users = $stmt->fetchAll(); // array of assoc arrays
foreach ($users as $user) {
echo "{$user['name']} — {$user['email']}\n";
}
// SELECT one
$stmt = $pdo->prepare("SELECT * FROM users WHERE id = ?");
$stmt->execute([1]);
$user = $stmt->fetch();
if ($user) {
echo $user['name'];
}
// UPDATE
$stmt = $pdo->prepare("UPDATE users SET name = ? WHERE id = ?");
$stmt->execute(["Alice Smith", 1]);
echo $stmt->rowCount() . " row(s) updated";
// DELETE
$stmt = $pdo->prepare("DELETE FROM users WHERE id = ?");
$stmt->execute([2]);
Transactions
<?php
try {
$pdo->beginTransaction();
$stmt = $pdo->prepare("INSERT INTO orders (user_id, total) VALUES (?, ?)");
$stmt->execute([1, 99.99]);
$orderId = (int) $pdo->lastInsertId();
$stmt = $pdo->prepare("INSERT INTO order_items (order_id, product_id, qty) VALUES (?, ?, ?)");
$stmt->execute([$orderId, 42, 2]);
$stmt = $pdo->prepare("UPDATE products SET stock = stock - ? WHERE id = ?");
$stmt->execute([2, 42]);
$pdo->commit();
echo "Order placed successfully";
} catch (PDOException $e) {
$pdo->rollBack();
error_log($e->getMessage());
echo "Order failed — rolled back";
}
Part 10 — Composer and Packages
Composer is PHP's package manager.
Install Composer
# Download installer
php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');"
php composer-setup.php
php -r "unlink('composer-setup.php');"
mv composer.phar /usr/local/bin/composer # Linux/Mac
Basic usage
composer init # Create composer.json
composer require guzzlehttp/guzzle # Install a package
composer require --dev phpunit/phpunit # Dev dependency
composer install # Install all from composer.json
composer update # Update to latest allowed versions
composer autoload # Regenerate autoloader
Autoloading
// composer.json
{
"autoload": {
"psr-4": {
"App\\": "src/"
}
}
}
composer dump-autoload
<?php
// index.php
require __DIR__ . '/vendor/autoload.php';
use App\Models\User;
$user = new User("Alice");
Popular PHP packages
| Package | Purpose | Install |
|---|---|---|
guzzlehttp/guzzle |
HTTP client | composer require guzzlehttp/guzzle |
monolog/monolog |
Logging | composer require monolog/monolog |
vlucas/phpdotenv |
.env files | composer require vlucas/phpdotenv |
ramsey/uuid |
UUID generation | composer require ramsey/uuid |
carbon/carbon |
Date/time | composer require nesbot/carbon |
phpmailer/phpmailer |
Email sending | composer require phpmailer/phpmailer |
league/csv |
CSV processing | composer require league/csv |
symfony/validator |
Validation | composer require symfony/validator |
Part 11 — Error Handling
<?php
declare(strict_types=1);
// Custom exception
class ValidationException extends RuntimeException {
public function __construct(
private array $errors,
string $message = "Validation failed"
) {
parent::__construct($message);
}
public function getErrors(): array {
return $this->errors;
}
}
function validateAge(int $age): void {
if ($age < 0 || $age > 150) {
throw new ValidationException(
["age" => "Age must be between 0 and 150"],
);
}
}
try {
validateAge(-5);
} catch (ValidationException $e) {
echo $e->getMessage() . "\n";
print_r($e->getErrors());
} catch (TypeError $e) {
echo "Type error: " . $e->getMessage();
} finally {
echo "Validation complete\n"; // always runs
}
// Set global error handler
set_exception_handler(function (Throwable $e) {
error_log($e->getMessage() . "\n" . $e->getTraceAsString());
http_response_code(500);
echo "Something went wrong";
});
Part 12 — Sessions and Cookies
<?php
// sessions.php
session_start(); // Must be called before any output
// Set session variables
$_SESSION['user_id'] = 1;
$_SESSION['username'] = 'Alice';
$_SESSION['logged_in'] = true;
// Read
$username = $_SESSION['username'] ?? 'guest';
// Delete one variable
unset($_SESSION['temp_data']);
// Destroy entire session (logout)
session_destroy();
// Regenerate session ID (prevent fixation attacks)
session_regenerate_id(true);
<?php
// Cookies
$thirtyDays = 60 * 60 * 24 * 30;
setcookie(
name: 'user_pref',
value: 'dark_mode',
expires: time() + $thirtyDays,
path: '/',
domain: '',
secure: true, // HTTPS only
httponly: true, // No JS access
);
// Read
$pref = $_COOKIE['user_pref'] ?? 'light_mode';
// Delete
setcookie('user_pref', '', time() - 3600);
Part 13 — Three Real Projects
Project 1 — CLI To-Do Manager
<?php
// todo.php
declare(strict_types=1);
const TODO_FILE = __DIR__ . '/todos.json';
function loadTodos(): array {
if (!file_exists(TODO_FILE)) return [];
return json_decode(file_get_contents(TODO_FILE), true) ?? [];
}
function saveTodos(array $todos): void {
file_put_contents(TODO_FILE, json_encode($todos, JSON_PRETTY_PRINT));
}
function addTodo(string $text): void {
$todos = loadTodos();
$todos[] = ['id' => uniqid(), 'text' => $text, 'done' => false];
saveTodos($todos);
echo "Added: $text\n";
}
function listTodos(): void {
$todos = loadTodos();
if (empty($todos)) { echo "No todos!\n"; return; }
foreach ($todos as $i => $todo) {
$status = $todo['done'] ? '[x]' : '[ ]';
echo ($i + 1) . ". $status {$todo['text']} ({$todo['id']})\n";
}
}
function completeTodo(int $index): void {
$todos = loadTodos();
$i = $index - 1;
if (!isset($todos[$i])) { echo "Invalid index\n"; return; }
$todos[$i]['done'] = true;
saveTodos($todos);
echo "Completed: {$todos[$i]['text']}\n";
}
function deleteTodo(int $index): void {
$todos = loadTodos();
$i = $index - 1;
if (!isset($todos[$i])) { echo "Invalid index\n"; return; }
$text = $todos[$i]['text'];
array_splice($todos, $i, 1);
saveTodos($todos);
echo "Deleted: $text\n";
}
// CLI entry point
$command = $argv[1] ?? 'list';
match ($command) {
'add' => addTodo(implode(' ', array_slice($argv, 2))),
'list' => listTodos(),
'done' => completeTodo((int) ($argv[2] ?? 0)),
'delete' => deleteTodo((int) ($argv[2] ?? 0)),
default => print("Usage: php todo.php [add|list|done|delete] [args]\n"),
};
php todo.php add "Buy groceries"
php todo.php add "Learn PHP"
php todo.php list
php todo.php done 1
php todo.php delete 2
Project 2 — Simple REST API (no framework)
<?php
// api.php — Simple REST API for notes
declare(strict_types=1);
header('Content-Type: application/json');
header('Access-Control-Allow-Origin: *');
// Simple in-memory storage (replace with DB in real app)
const DATA_FILE = __DIR__ . '/notes.json';
function loadNotes(): array {
if (!file_exists(DATA_FILE)) return [];
return json_decode(file_get_contents(DATA_FILE), true) ?? [];
}
function saveNotes(array $notes): void {
file_put_contents(DATA_FILE, json_encode(array_values($notes)));
}
function respond(mixed $data, int $code = 200): never {
http_response_code($code);
echo json_encode($data);
exit;
}
function getBody(): array {
$raw = file_get_contents('php://input');
return json_decode($raw, true) ?? [];
}
$method = $_SERVER['REQUEST_METHOD'];
$path = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
$parts = array_filter(explode('/', trim($path, '/')));
$parts = array_values($parts);
// Routes: GET /notes, POST /notes, GET /notes/{id}, PUT /notes/{id}, DELETE /notes/{id}
if ($parts[0] !== 'notes') {
respond(['error' => 'Not found'], 404);
}
$notes = loadNotes();
$id = $parts[1] ?? null;
match ([$method, $id !== null]) {
['GET', false] => respond(array_values($notes)),
['GET', true] => isset($notes[$id])
? respond($notes[$id])
: respond(['error' => 'Not found'], 404),
['POST', false] => (function() use (&$notes) {
$body = getBody();
$note = ['id' => uniqid(), 'title' => $body['title'] ?? '', 'body' => $body['body'] ?? ''];
$notes[$note['id']] = $note;
saveNotes($notes);
respond($note, 201);
})(),
['PUT', true] => (function() use (&$notes, $id) {
if (!isset($notes[$id])) respond(['error' => 'Not found'], 404);
$body = getBody();
$notes[$id] = array_merge($notes[$id], $body);
saveNotes($notes);
respond($notes[$id]);
})(),
['DELETE', true] => (function() use (&$notes, $id) {
if (!isset($notes[$id])) respond(['error' => 'Not found'], 404);
unset($notes[$id]);
saveNotes($notes);
respond(['message' => 'Deleted']);
})(),
default => respond(['error' => 'Method not allowed'], 405),
};
php -S localhost:8000 api.php
# Test with curl
curl -X POST http://localhost:8000/notes \
-H 'Content-Type: application/json' \
-d '{"title":"First note","body":"Hello PHP!"}'
curl http://localhost:8000/notes
Project 3 — User Login System (PDO + Sessions)
<?php
// auth.php — complete login/registration with hashed passwords
declare(strict_types=1);
session_start();
// PDO setup
function getDb(): PDO {
static $pdo = null;
if ($pdo === null) {
$pdo = new PDO('sqlite:' . __DIR__ . '/auth.db', null, null, [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
]);
$pdo->exec("CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
email TEXT NOT NULL UNIQUE,
password TEXT NOT NULL
)");
}
return $pdo;
}
function register(string $name, string $email, string $password): string {
if (strlen($name) < 2) return "Name too short";
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) return "Invalid email";
if (strlen($password) < 8) return "Password too short";
$pdo = getDb();
$hash = password_hash($password, PASSWORD_BCRYPT);
try {
$stmt = $pdo->prepare("INSERT INTO users (name, email, password) VALUES (?, ?, ?)");
$stmt->execute([$name, $email, $hash]);
return "ok";
} catch (PDOException) {
return "Email already registered";
}
}
function login(string $email, string $password): bool {
$pdo = getDb();
$stmt = $pdo->prepare("SELECT * FROM users WHERE email = ?");
$stmt->execute([$email]);
$user = $stmt->fetch();
if ($user && password_verify($password, $user['password'])) {
session_regenerate_id(true);
$_SESSION['user_id'] = $user['id'];
$_SESSION['name'] = $user['name'];
return true;
}
return false;
}
function logout(): void {
session_unset();
session_destroy();
}
function currentUser(): ?array {
return isset($_SESSION['user_id'])
? ['id' => $_SESSION['user_id'], 'name' => $_SESSION['name']]
: null;
}
// Simple router
$action = $_POST['action'] ?? $_GET['action'] ?? '';
switch ($action) {
case 'register':
$result = register(
$_POST['name'] ?? '',
$_POST['email'] ?? '',
$_POST['password'] ?? ''
);
echo $result === 'ok' ? 'Registered!' : "Error: $result";
break;
case 'login':
$ok = login($_POST['email'] ?? '', $_POST['password'] ?? '');
echo $ok ? 'Logged in as ' . $_SESSION['name'] : 'Invalid credentials';
break;
case 'logout':
logout();
echo 'Logged out';
break;
case 'me':
$user = currentUser();
echo $user ? "Hello, {$user['name']}!" : "Not logged in";
break;
default:
echo 'Auth API: register | login | logout | me';
}
Quick Reference
PHP operators
| Category | Operators |
|---|---|
| Arithmetic | + - * / % ** |
| Comparison | == === != !== < > <= >= <=> |
| Logical | && || ! and or xor |
| String | . (concatenate) .= (append) |
| Assignment | = += -= *= /= .= ??= |
| Null coalescing | ?? |
Useful PHP functions
| Function | Purpose |
|---|---|
var_dump($x) |
Debug any variable (type + value) |
print_r($arr) |
Human-readable array/object output |
json_encode($data) |
PHP → JSON string |
json_decode($str, true) |
JSON string → PHP array |
date('Y-m-d') |
Current date as string |
time() |
Current Unix timestamp |
rand(1, 100) |
Random integer |
array_key_exists($k, $arr) |
Key exists? |
is_array($x) / is_null($x) |
Type checks |
empty($x) |
Is empty? (null/0/''/'0'/[]/false) |
isset($x) |
Is set and not null? |
intval($x) / floatval($x) |
Convert to int/float |
number_format(1234.5, 2) |
Format number "1,234.50" |
sprintf("%.2f", 3.14159) |
Format string |
str_pad($s, 10, '0', STR_PAD_LEFT) |
Pad string |
explode(',', $str) |
String → array |
implode(', ', $arr) |
Array → string |
preg_match('/\d+/', $str) |
Regex match |
preg_replace('/\s+/', ' ', $str) |
Regex replace |
file_get_contents($path) |
Read file |
file_put_contents($path, $data) |
Write file |
is_file($path) / is_dir($path) |
File/dir check |
glob('*.php') |
List files by pattern |
Learning Path
| Stage | What to learn | Time |
|---|---|---|
| 1 | Syntax, variables, control flow, functions | 1–2 weeks |
| 2 | Arrays, strings, forms, superglobals, security basics | 1–2 weeks |
| 3 | OOP: classes, inheritance, interfaces, traits | 2–3 weeks |
| 4 | MySQL/PDO, prepared statements, CRUD | 1–2 weeks |
| 5 | Composer, PSR standards, autoloading | 1 week |
| 6 | Laravel or Symfony basics | 4–8 weeks |
| 7 | REST APIs, JWT auth, testing (PHPUnit) | 3–4 weeks |
| 8 | Production: queues, caching, Docker | Ongoing |
Free resources
| Resource | URL |
|---|---|
| PHP.net docs | php.net/manual |
| PHP: The Right Way | phptherightway.com |
| Laracasts (free tier) | laracasts.com |
| PHP School | phpschool.io |
| Laravel docs | laravel.com/docs |
| Symfony docs | symfony.com/doc |
| PHPUnit docs | phpunit.de |
Common Mistakes
| Mistake | Problem | Fix |
|---|---|---|
Using == instead of === |
"0" == false is true (loose comparison) |
Use === for strict type+value check |
| Echoing user input directly | XSS vulnerability | Always htmlspecialchars() before output |
| String interpolation in SQL | SQL injection | Use PDO prepared statements |
$_POST without sanitization |
Security risk | Validate + sanitize all inputs |
Not calling session_start() |
Sessions don't work | Call at top of every file that uses sessions |
Suppressing errors with @ |
Hides real problems | Fix the root cause; use try/catch |
No declare(strict_types=1) |
Silent type coercions | Always declare at top of files |
| Storing plain text passwords | Critical security flaw | Use password_hash() / password_verify() |
PHP vs Related Terms
| Term | What it is |
|---|---|
| PHP | Server-side scripting language |
| PHP 8 | Current major version (8.0–8.4) with JIT, named args, enums |
| Composer | PHP's package manager (like npm for JS) |
| Packagist | Central package repository for Composer |
| Laravel | Most popular PHP framework (Rails-like) |
| Symfony | Enterprise PHP framework (powers Laravel internals) |
| WordPress | CMS built on PHP (43% of all websites) |
| PSR | PHP Standards Recommendations (coding standards) |
| Eloquent | Laravel's ORM for database access |
| Artisan | Laravel's CLI (like Rails rails command) |
| PDO | PHP's built-in database abstraction layer |
| Twig / Blade | Templating engines for Symfony / Laravel |
FAQ
Q: Is PHP still relevant in 2025?
Yes. PHP 8.x brought named arguments, attributes, enums, fibers, readonly properties, and a JIT compiler. Laravel is one of the most productive web frameworks available. PHP powers ~77% of sites with a known server-side language and has a massive job market.
Q: Should I learn PHP or JavaScript (Node.js)?
Learn both eventually. Start with PHP if you want to work with WordPress/WooCommerce, need shared hosting, or are building a traditional web app. Start with Node.js if you want a unified JS stack or are focused on real-time apps. They are complementary, not competing.
Q: What's the difference between PHP and HTML?
HTML is a markup language that browsers render. PHP is a programming language that runs on the server, generates HTML (or JSON), and sends it to the browser. The browser never sees PHP code.
Q: Do I need a framework to build PHP apps?
No, but you should use one for anything beyond small scripts. Laravel is the standard choice for new projects. Symfony is preferred for large enterprise apps. Start with plain PHP to understand the fundamentals, then move to Laravel.
Q: What is the difference between include and require?
include shows a warning and continues if the file is missing. require throws a fatal error and stops execution. Use require for files your app cannot run without. Use include_once / require_once to prevent double-inclusion.
Q: How long does it take to learn PHP?
You can build functional web apps within 4–6 weeks of consistent study. To be job-ready (with Laravel, MySQL, REST APIs, testing), expect 3–6 months of practice. PHP has a gentle learning curve compared to statically typed languages.