Back to PHP Examples

Basic PHP

A quick-reference and hands-on guide to the fundamentals of PHP programming.

How to use this page

The fastest way to keep what you learn is to teach it. The physicist Richard Feynman's trick was simple: if you can't explain something in plain words, you don't really understand it yet. For each section, work these four steps:

  1. Read the lesson once.
  2. Explain it out loud in plain words, as if teaching a friend — no jargon. If you stall, that's the gap.
  3. Do the exercise from a blank editor, without peeking. Where you get stuck is exactly what to reread.
  4. Reveal the solution, compare, and explain the idea again in your own words.

Every exercise is a complete script. Save it as practice.php and run it from a terminal with php practice.php (PHP 8.1 or newer). Watch for the 💡 Explain it simply prompts and ✎ Exercise boxes in each section. Hover any code block and click Copy to grab it.

Contents

  1. What is PHP?
  2. Syntax & Structure
  3. Variables & Data Types
  4. Operators
  5. Strings
  6. Arrays
  7. Control Structures
  8. Loops
  9. Functions
  10. Forms & User Input
  11. Intro to OOP

1. What is PHP?

A programming language is a precise, written notation for telling a computer what to do. PHP began in 1994 as a handful of scripts Rasmus Lerdorf wrote to track visits to his online résumé. He called them “Personal Home Page” tools. The name now stands for the recursive PHP: Hypertext Preprocessor, and the language has grown into one of the most widely deployed on the web: it runs WordPress, Wikipedia, and Facebook's original codebase, and powers a large share of all websites. Modern PHP (version 8 and later) is fast, has optional strict typing, and supports classes, interfaces, and anonymous functions.

PHP is a server-side scripting language. When a browser requests a .php page, the web server hands the file to the PHP interpreter, which runs the code and sends back only the result, usually HTML. The visitor never sees the PHP source. This is the key difference from JavaScript running in the browser: PHP runs on the server, before the page is sent, so it can read databases, check passwords, and build each page fresh for each request.

PHP is interpreted: there is no separate compile step, so you edit a file and reload the page. It is also dynamically typed: variables don't declare a type, and values are converted between types automatically when needed. That makes PHP quick to start with, but it's also the source of its best-known surprises, which is why modern PHP code adds type declarations and uses strict comparisons. You can also run PHP from the command line (php script.php), which is how every exercise on this page works.

Server-side
Code that runs on the web server, sending only its output to the browser.
Interpreter
The program that reads and executes PHP source directly, with no separate compile step.
Request
One browser visit to a URL; PHP runs the script fresh for each one.
Explain it simply

If you “View Source” on a PHP page in your browser, why don't you see any PHP code? Explain it with a restaurant.

Reveal a plain-language answer

The PHP is the recipe, and it stays in the kitchen (the server). The kitchen follows the recipe and sends out only the finished dish, the HTML. Your browser is the diner: it only ever sees the plate, never the recipe card. That's also why PHP can safely contain things like database passwords that visitors must never see.

2. Syntax & Structure

A PHP file is really a template. Anything outside PHP tags is sent to the output exactly as written, and code between the opening tag <?php and the closing tag ?> is executed. This lets you drop small pieces of logic into an otherwise ordinary HTML page, which is how PHP was originally designed to be used. A file that contains only PHP code should omit the closing ?> at the end, because any stray whitespace after it would be sent as output, which can break things such as HTTP headers.

Inside the tags, PHP looks much like C or JavaScript. Each statement ends with a semicolon, curly braces group statements into blocks, and whitespace and indentation are for human readers. echo is the everyday way to send output. It's a language construct rather than a function, so it doesn't need parentheses, and it accepts several comma-separated values. Comments are written // like this, # like this, or /* across several lines */.

Keywords and function names are case-insensitive (ECHO works), but variable names are case-sensitive: $name and $Name are different variables. One small detail explains some confusing output: PHP swallows a single newline directly after a closing ?> tag. That's why the output in this section's exercise has no blank line where the tag was.

PHP tags
<?php … ?>: code between them runs; everything else is output as-is.
echo
The language construct that writes output: echo "Hi", "\n";.
Statement
One instruction, ended by a semicolon.
<!-- index.php -->
<?php
    // Single-line comment
    # Also a single-line comment
    /* Multi-line
       comment */

    echo "Hello, World!";
?>
PHP can be mixed with HTML. Anything outside <?php ?> tags is sent to the browser as-is.
Exercise — your turn

Save this as page.php and run php page.php. Fill in the one missing line so the PHP block prints <p>Hello, Ada!</p> between the two plain HTML lines.

<h1>Welcome</h1>
<?php
$name = "Ada";
// TODO: echo "<p>Hello, $name!</p>" followed by a newline
?>
<p>Bye.</p>
Show solution
<h1>Welcome</h1>
<?php
$name = "Ada";
echo "<p>Hello, $name!</p>\n";
?>
<p>Bye.</p>
Output
<h1>Welcome</h1>
<p>Hello, Ada!</p>
<p>Bye.</p>
Explain it simply

How can one .php file contain both plain HTML and PHP code? Explain it with a fill-in-the-blanks letter.

Reveal a plain-language answer

It's a form letter: most of the text is printed exactly as written, and the <?php … ?> parts are the blanks. PHP walks through the file top to bottom, copying the ordinary text straight to the output and, whenever it reaches a blank, running the code there and printing whatever that code echoes in its place.

3. Variables & Data Types

A variable is a named container for a value. In PHP every variable name begins with a dollar sign, as in $name, followed by a letter or underscore. You create a variable simply by assigning to it with =; there is no declaration keyword and no type to state. The $ makes variables easy to spot in code and lets PHP insert them directly into double-quoted strings: "Hello, $name".

Every value has a type. PHP's scalar types are string (text), int (whole numbers), float (numbers with a fractional part; gettype() reports these as "double" for historical reasons), and bool (true or false). There is also null, meaning “no value”, and the compound types array and object. Because PHP is dynamically typed, the type belongs to the value, not the variable, so the same variable can hold a number now and a string later. var_dump() shows both a value and its type, which makes it the most useful debugging tool for beginners.

PHP performs type juggling: it automatically converts values when an operation needs a different type. "5" + 3 is 8, because the string is read as a number. You can also convert explicitly with a cast, as in (int) "42 apples", which reads the leading digits and gives 42. Automatic conversion is convenient but can hide mistakes, so modern PHP lets you declare types on function parameters and return values and turn on declare(strict_types=1); to make those checks strict. Constants, defined with const NAME = value;, have no $ and can never change.

$variable
Every PHP variable name starts with $ and is case-sensitive.
Type juggling
PHP automatically converting a value to the type an operation needs.
var_dump()
Prints a value together with its type, e.g. bool(false).
<?php
$name    = "Alice";      // String
$age     = 30;           // Integer
$price   = 9.99;         // Float
$active  = true;         // Boolean
$nothing = null;         // Null

echo $name;               // Alice
var_dump($age);           // int(30)
gettype($price);          // "double"
?>
TypeExampleDescription
string"hello"Text, in single or double quotes
int42Whole numbers
float3.14Decimal numbers
booltrue / falseBoolean values
nullnullNo value
array[1, 2, 3]Ordered collection
objectnew MyClass()Instance of a class
Exercise — your turn

Create the four variables so the first line prints as shown, then see type juggling at work: the last three lines use gettype, a numeric string added to an integer, and an (int) cast.

<?php
// TODO: create $name ("Ada"), $age (36), $height (1.65) and $isAdmin (false)

echo "$name is $age years old and $height m tall.\n";
var_dump($isAdmin);
echo gettype($height), "\n";
echo "5" + 3, "\n";
echo (int) "42 apples", "\n";
Show solution
<?php
$name = "Ada";
$age = 36;
$height = 1.65;
$isAdmin = false;

echo "$name is $age years old and $height m tall.\n";
var_dump($isAdmin);
echo gettype($height), "\n";
echo "5" + 3, "\n";
echo (int) "42 apples", "\n";
Output
Ada is 36 years old and 1.65 m tall.
bool(false)
double
8
42
Explain it simply

Why does "5" + 3 give 8 in PHP instead of an error? Is that helpful or dangerous?

Reveal a plain-language answer

PHP sees +, which only makes sense for numbers, so it helpfully reads the text "5" as the number 5. That's convenient with web forms, where everything arrives as text. But it's dangerous when the text isn't what you expected: a typo or odd input can quietly turn into a number you never meant. That's why careful PHP code validates input and uses type declarations.

4. Operators

An operator is a symbol that performs an operation on one or more values, its operands. PHP's arithmetic operators are + - * / % plus ** for exponentiation. Unlike C, / gives a float whenever the result isn't a whole number, so 17 / 5 is 3.4. When you want whole-number division, use intdiv(17, 5), and % for the remainder. PHP also has a dedicated operator for joining strings: the dot, ., with its assignment form .=. + is only ever arithmetic.

Comparison is where PHP needs the most care, because there are two kinds of equality. The loose operator == converts both sides to a common type before comparing, so "5" == 5 is true. The strict operator === compares both value and type, so "5" === 5 is false. Their negations are != and !==. Loose comparison has produced many real bugs and even security holes, so the rule of thumb is: always use === unless you have a specific reason not to. The spaceship operator <=> returns -1, 0, or 1, which is handy for sorting.

The logical operators &&, ||, and ! combine conditions. (PHP also has and and or, but they have surprisingly low precedence, so stick with the symbols.) The null coalescing operator ?? returns its left side unless it is null or undefined, in which case it returns the right side, without raising a warning about a missing variable or array key. That makes it ideal for reading optional input: $_GET["page"] ?? 1.

Concatenation
Joining strings with the . operator: "a" . "b" is "ab".
== vs ===
Loose equality (converts types first) / strict equality (value and type).
??
Null coalescing: the left value, unless it's null or missing.

Arithmetic

<?php
$a = 10; $b = 3;
echo $a + $b;   // 13  — addition
echo $a - $b;   // 7   — subtraction
echo $a * $b;   // 30  — multiplication
echo $a / $b;   // 3.33 — division
echo $a % $b;   // 1   — modulus (remainder)
echo $a ** $b;  // 1000 — exponentiation
?>

Comparison

<?php
$x ==  $y   // Equal (value only)
$x === $y   // Identical (value AND type)
$x !=  $y   // Not equal
$x !== $y   // Not identical
$x <   $y   // Less than
$x >   $y   // Greater than
$x <=  $y   // Less than or equal
$x >=  $y   // Greater than or equal
?>
Always prefer === over == to avoid type coercion surprises.

Logical

<?php
$a && $b   // AND — true if both are true
$a || $b   // OR  — true if either is true
!$a        // NOT — inverts the boolean
?>
Exercise — your turn

A carton holds 5 eggs. Fill in the four expressions, then predict the two var_dump results before running it: which comparison is loose and which is strict?

<?php
$eggs = 17;
$perBox = 5;

echo "Full boxes: " . 0 . "\n";     // TODO: intdiv
echo "Left over: " . 0 . "\n";      // TODO: %
echo "Exact: " . 0 . "\n";          // TODO: /
echo "2 ** 10 = " . 0 . "\n";       // TODO: **
var_dump("5" == 5);
var_dump("5" === 5);
Show solution
<?php
$eggs = 17;
$perBox = 5;

echo "Full boxes: " . intdiv($eggs, $perBox) . "\n";
echo "Left over: " . $eggs % $perBox . "\n";
echo "Exact: " . $eggs / $perBox . "\n";
echo "2 ** 10 = " . 2 ** 10 . "\n";
var_dump("5" == 5);
var_dump("5" === 5);
Output
Full boxes: 3
Left over: 2
Exact: 3.4
2 ** 10 = 1024
bool(true)
bool(false)
Explain it simply

Explain the difference between == and === using two coins.

Reveal a plain-language answer

== asks “are these worth the same?”: a paper $1 bill and a $1 coin pass, because PHP converts them to a common form first. === asks “are these worth the same and the same kind of thing?”: the bill and the coin fail. "5" is text and 5 is a number, so they're equal in worth but not identical.

5. Strings

A string is a sequence of characters, the type PHP uses for all text. PHP has two quoting styles, and they behave differently. Single-quoted strings are taken literally: 'Hello $name\n' prints exactly those characters. Double-quoted strings are interpolated: variables inside them are replaced with their values, and escape sequences such as \n (newline) and \t (tab) are converted. For anything more complex than a plain variable, such as an array element or an object property, wrap it in braces: "Hi {$user['name']}". For long blocks of text, heredoc syntax (<<<EOT) behaves like a multi-line double-quoted string.

Strings are joined with the dot operator, .. PHP has a very large built-in library of string functions. You'll use these constantly: strlen (length), strtoupper/strtolower, ucfirst and ucwords (capitalise), trim (remove surrounding whitespace), str_replace, str_contains and strpos (search), substr (extract part), explode (split into an array), and implode (join an array into a string). A single character can be read by index: $s[0].

Two details matter in practice. First, strlen counts bytes, not characters, so text with accents or emoji needs the multibyte versions such as mb_strlen and mb_strtoupper. Second, printf and sprintf provide C-style formatting with placeholders such as %s (string), %d (integer), and %.2f (two decimal places), plus widths for aligning columns. printf prints the result; sprintf returns it as a string.

Interpolation
Replacing $variables inside a double-quoted string with their values.
explode / implode
Split a string into an array / join an array into a string.
sprintf
Builds a formatted string from a pattern such as "%-8s%6.2f".
<?php
$first = "Alice";
$last  = "Smith";

// Concatenation uses the dot operator
echo $first . " " . $last;   // Alice Smith

// Variable interpolation (double quotes only)
echo "Hello, $first!";        // Hello, Alice!
echo "Hello, {$first}!";      // Same, clearer syntax

// Common string functions
strlen("hello");              // 5
strtoupper("hello");          // HELLO
strtolower("HELLO");          // hello
trim("  hello  ");           // "hello"
str_replace("o", "0", "foo"); // "f00"
substr("hello", 1, 3);        // "ell"
strpos("hello", "l");         // 2 (first occurrence)
explode(",", "a,b,c");        // ["a","b","c"]
implode("-", ["a","b"]);      // "a-b"
?>
Exercise — your turn

Clean up the messy name so it prints as “Ada Lovelace” with its length, build the initials with explode and a loop, then print a padded receipt line with printf.

<?php
$raw = "  ada lovelace  ";

$full = $raw;       // TODO: trim, then capitalise each word
$initials = "";
// TODO: explode $full on " " and append each word's first letter plus "."

echo "$full (" . strlen($full) . " chars)\n";
echo "Initials: $initials\n";
// TODO: printf "Coffee" left-aligned in 8 columns and 3.5 in 6 columns with
//       2 decimals, between | characters: "%-8s|%6.2f|\n"
Show solution
<?php
$raw = "  ada lovelace  ";

$full = ucwords(trim($raw));
$initials = "";
foreach (explode(" ", $full) as $word) {
    $initials .= $word[0] . ".";
}

echo "$full (" . strlen($full) . " chars)\n";
echo "Initials: $initials\n";
printf("%-8s|%6.2f|\n", "Coffee", 3.5);
Output
Ada Lovelace (12 chars)
Initials: A.L.
Coffee  |  3.50|
Explain it simply

Why does echo 'Hi $name'; print a dollar sign while echo "Hi $name"; prints the name? Explain the two kinds of quotes.

Reveal a plain-language answer

Single quotes are a sealed envelope: PHP passes the contents along exactly as written, dollar signs and all. Double quotes are an open form that PHP reads and fills in: every $name is swapped for the variable's value and every \n for a real line break. Use single quotes when you want text taken literally, and double quotes when you want PHP to fill in the blanks.

6. Arrays

PHP has one main collection type, the array, and it is remarkably versatile. Strictly speaking, every PHP array is an ordered map: a list of key–value pairs that remembers the order you inserted them in. That one structure plays the role of what other languages split into lists, dictionaries, stacks, and queues. Arrays are written with square brackets, [1, 2, 3], and can hold values of any type, including other arrays.

An indexed array uses integer keys that PHP assigns automatically, starting at 0: in $fruits = ["apple", "banana"], $fruits[0] is "apple". Writing $fruits[] = "cherry"; appends to the end. An associative array uses keys you choose, usually strings: $person = ["name" => "Ada", "age" => 36], read with $person["name"]. Reading a key that doesn't exist raises a warning, so use isset($a["k"]), array_key_exists, or $a["k"] ?? $default when a key may be missing.

PHP includes dozens of array functions. The essentials are count (number of elements), in_array (search by value), array_keys and array_values, array_slice (part of an array), array_merge, and the sorting family: sort/rsort for values, and asort/ksort to sort associative arrays by value or key while keeping pairs together. Note that the sorting functions change the array in place and return only true. To look inside an array while debugging, use print_r or var_dump.

Indexed array
An array with automatic integer keys 0, 1, 2…
Associative array
An array with keys you choose: ["name" => "Ada"].
Ordered map
What every PHP array really is: key–value pairs that keep insertion order.

Indexed Arrays

<?php
$fruits = ["apple", "banana", "cherry"];
echo $fruits[0];              // apple
$fruits[] = "date";          // append
count($fruits);              // 4
?>

Associative Arrays

<?php
$person = [
    "name" => "Alice",
    "age"  => 30,
    "city" => "NYC",
];

echo $person["name"];         // Alice
$person["email"] = "a@b.com"; // add key
array_keys($person);         // ["name","age","city","email"]
array_values($person);       // ["Alice",30,"NYC","a@b.com"]
array_key_exists("age", $person); // true
?>

Useful Array Functions

<?php
sort($arr);                   // sort indexed array ascending
rsort($arr);                  // sort descending
asort($arr);                  // sort assoc by value
ksort($arr);                  // sort assoc by key
in_array("apple", $fruits);  // true
array_push($arr, "x");        // append
array_pop($arr);              // remove last
array_merge($a, $b);         // merge two arrays
array_slice($arr, 1, 3);      // extract sub-array
?>
Exercise — your turn

Append 95, sort, and print the scores and the top two. Then count words into an associative array, using ?? so a word seen for the first time starts at 0.

<?php
$scores = [88, 92, 75];
// TODO: append 95, then sort
echo "Scores: " . implode(", ", $scores) . "\n";
echo "Top two: " . implode(", ", $scores) . "\n";   // TODO: array_slice the last two

$counts = [];
foreach (explode(" ", "the cat and the hat") as $word) {
    // TODO: add one to $counts[$word], treating a missing key as 0
}
echo "the = {$counts['the']}, cat = {$counts['cat']}\n";
echo "Words: " . implode(", ", array_keys($counts)) . "\n";
Show solution
<?php
$scores = [88, 92, 75];
$scores[] = 95;
sort($scores);
echo "Scores: " . implode(", ", $scores) . "\n";
echo "Top two: " . implode(", ", array_slice($scores, -2)) . "\n";

$counts = [];
foreach (explode(" ", "the cat and the hat") as $word) {
    $counts[$word] = ($counts[$word] ?? 0) + 1;
}
echo "the = {$counts['the']}, cat = {$counts['cat']}\n";
echo "Words: " . implode(", ", array_keys($counts)) . "\n";
Output
Scores: 75, 88, 92, 95
Top two: 92, 95
the = 2, cat = 1
Words: the, cat, and, hat
Explain it simply

PHP uses one “array” for both lists and dictionaries. How can one structure do both? Explain it with coat-check tickets.

Reveal a plain-language answer

Every PHP array is a coat check: each item hangs on a hook with a ticket (the key). For a list, PHP just hands out tickets numbered 0, 1, 2 in order. For a dictionary, you write your own tickets, such as "name" or "age". Either way you get the coat back by showing the ticket, and the rack remembers the order the coats arrived in.

7. Control Structures

Statements normally run top to bottom. Control structures let a script choose which code runs based on conditions. In a web application this is how one script serves every visitor differently: logged in or not, form valid or not, page found or not.

The if statement evaluates a condition in parentheses and runs the following block only if the condition is truthy. elseif adds further tests, and else catches everything left. PHP checks the branches in order and runs only the first that matches. Because of type juggling, many values count as false in a condition: false, 0, 0.0, "", "0", an empty array, and null. Everything else is true. For a compact two-way choice, the ternary operator $cond ? $a : $b works inside an expression.

When one value is compared with many possibilities, PHP offers two tools. The classic switch statement jumps to a matching case but compares loosely (==) and falls through into the next case unless you write break. PHP 8 introduced the match expression, which fixes both problems: it compares strictly (===), never falls through, and returns a value, so you can write $name = match ($day) { 1 => "Monday", … };. If no arm matches and there is no default, it throws an error instead of silently doing nothing.

Truthy / falsy
Whether a value counts as true or false in a condition; 0, "", "0", [], and null are falsy.
match
A PHP 8 expression that compares strictly and returns the value of the matching arm.
Fall-through
In switch, running on into the next case when there's no break.

if / elseif / else

<?php
$score = 75;

if ($score >= 90) {
    echo "A";
} elseif ($score >= 75) {
    echo "B";
} elseif ($score >= 60) {
    echo "C";
} else {
    echo "F";
}
?>

switch

<?php
$day = "Mon";

switch ($day) {
    case "Mon":
        echo "Monday";
        break;
    case "Fri":
        echo "Friday";
        break;
    default:
        echo "Other day";
}
?>

Ternary & Null Coalescing

<?php
// Ternary: condition ? if_true : if_false
$status = $age >= 18 ? "adult" : "minor";

// Null coalescing: use right side if left is null/unset
$name = $_GET["name"] ?? "Guest";
?>
Exercise — your turn

Finish grade with if/elseif/else (90+ A, 80+ B, 70+ C, 60+ D, else F), turn the day number into a name with match, and greet with ??.

<?php
function grade(int $score): string {
    // TODO: if / elseif / else returning "A", "B", "C", "D" or "F"
}

echo grade(95), " ", grade(82), " ", grade(64), " ", grade(40), "\n";

$day = 3;
$name = "";   // TODO: match ($day) { 1 => "Monday", 2 => ..., 3 => ..., default => "some other day" }
echo "Day $day is $name\n";

$nickname = null;
echo "Hello, " . "" . "\n";   // TODO: $nickname, or "friend" if it's null
Show solution
<?php
function grade(int $score): string {
    if ($score >= 90) {
        return "A";
    } elseif ($score >= 80) {
        return "B";
    } elseif ($score >= 70) {
        return "C";
    } elseif ($score >= 60) {
        return "D";
    } else {
        return "F";
    }
}

echo grade(95), " ", grade(82), " ", grade(64), " ", grade(40), "\n";

$day = 3;
$name = match ($day) {
    1 => "Monday",
    2 => "Tuesday",
    3 => "Wednesday",
    default => "some other day",
};
echo "Day $day is $name\n";

$nickname = null;
echo "Hello, " . ($nickname ?? "friend") . "\n";
Output
A B D F
Day 3 is Wednesday
Hello, friend
Explain it simply

Why was match added when PHP already had switch? Name two things it does better.

Reveal a plain-language answer

First, switch compares loosely, so "1" and 1 count as the same case, which can pick the wrong branch. match compares strictly. Second, switch keeps running into the next case if you forget a break, while match runs exactly one arm and hands back its value. As a bonus, an unmatched value in match raises an error instead of silently doing nothing.

8. Loops

A loop repeats a block of code. In web programming, loops are everywhere: printing every row of a database result as an HTML table row, every product on a page, or every error message from a form. Each pass through the loop body is an iteration.

PHP has four loops. The while loop repeats as long as its condition stays true, checking before each pass, so it may run zero times. The do … while loop checks after each pass, so it always runs at least once. The for loop puts a counter's initialisation, condition, and update on one line, for ($i = 0; $i < 10; $i++), and suits counting a known number of times. PHP has ++ and -- operators for this.

The loop you'll use most is foreach, which walks through an array without any counter to manage. foreach ($items as $item) gives you each value in turn, and foreach ($stock as $name => $count) gives you each key and value, which is ideal for associative arrays. By default foreach works on a copy of each value; to modify the array's elements in place, loop by reference with &$item, and unset($item) afterwards. Inside any loop, break exits early and continue skips to the next iteration.

Iteration
One pass through a loop's body.
foreach
Loops over an array, giving each value (and optionally its key) in turn.
Off-by-one error
Looping one time too many or too few, often <= vs <.

while

<?php
$i = 1;
while ($i <= 5) {
    echo $i;
    $i++;
}
// Output: 12345
?>

for

<?php
for ($i = 0; $i < 5; $i++) {
    echo $i . " ";
}
// Output: 0 1 2 3 4
?>

foreach (great for arrays)

<?php
$fruits = ["apple", "banana", "cherry"];

foreach ($fruits as $fruit) {
    echo $fruit . "<br>";
}

// With key => value for associative arrays
$person = ["name" => "Alice", "age" => 30];
foreach ($person as $key => $value) {
    echo "$key: $value<br>";
}
?>
Use break to exit a loop early, and continue to skip to the next iteration.
Exercise — your turn

Write FizzBuzz for 1 to 15 with a for loop (“Fizz” for multiples of 3, “Buzz” for 5, “FizzBuzz” for both), then print each fruit and its count with foreach using $key => $value.

<?php
$parts = [];
// TODO: for $i from 1 to 15, append "FizzBuzz", "Fizz", "Buzz" or $i to $parts
echo implode(" ", $parts), "\n";

$stock = ["apple" => 3, "banana" => 5];
// TODO: foreach over $stock printing "apple: 3" and "banana: 5"
Show solution
<?php
$parts = [];
for ($i = 1; $i <= 15; $i++) {
    if ($i % 15 === 0) {
        $parts[] = "FizzBuzz";
    } elseif ($i % 3 === 0) {
        $parts[] = "Fizz";
    } elseif ($i % 5 === 0) {
        $parts[] = "Buzz";
    } else {
        $parts[] = $i;
    }
}
echo implode(" ", $parts), "\n";

$stock = ["apple" => 3, "banana" => 5];
foreach ($stock as $fruit => $count) {
    echo "$fruit: $count\n";
}
Output
1 2 Fizz 4 Buzz Fizz 7 8 Fizz Buzz 11 Fizz 13 14 FizzBuzz
apple: 3
banana: 5
Explain it simply

When would you choose foreach over a for loop with a counter? Explain it with a stack of mail.

Reveal a plain-language answer

foreach is going through the mail pile one letter at a time: you just take the next letter until there aren't any, with no numbering and no way to miscount. A for loop is numbering the letters and fetching letter 0, 1, 2… by number. That's useful when you actually need the numbers, but it's easy to stop one short or go one past. For “do something with every item”, foreach is simpler and safer.

9. Functions

A function is a named, reusable block of code that performs one task. You define one with the function keyword, a name, a list of parameters in parentheses, and a body in braces, and you send a result back with return. PHP ships with thousands of built-in functions (strlen, count, array_map), and writing your own lets you name a piece of logic, test it on its own, and reuse it instead of copying code between pages.

Modern PHP lets you add type declarations to parameters and return values: function maxOf(int $a, int $b): int. With them, PHP checks what goes in and comes out, and your editor can catch mistakes. A void return type means the function returns nothing. Parameters can have default values (string $name = "World"), which makes them optional, and a variadic parameter written ...$nums collects any number of arguments into an array. Since PHP 8 you can also pass arguments by name, as in greet(name: "Ada").

Variables inside a function are local. A function cannot see variables from the surrounding script unless they're passed in as arguments, which keeps functions self-contained and predictable. Arguments are passed by value (the function gets a copy) unless the parameter is declared by reference with &. PHP also supports anonymous functions, and since PHP 7.4 the short arrow function fn($n) => $n * 2, which is ideal for passing a small rule to functions such as array_map, array_filter, and usort.

Type declaration
A type on a parameter or return value, e.g. int $a or : string.
Local scope
Variables inside a function exist only there and can't see the outer script's variables.
Arrow function
fn($x) => expr, a short anonymous function that returns expr.
<?php
// Basic function
function greet($name) {
    return "Hello, $name!";
}
echo greet("Alice");          // Hello, Alice!

// Default parameter values
function greet($name = "World") {
    return "Hello, $name!";
}
echo greet();                 // Hello, World!

// Type hints (PHP 7+)
function add(int $a, int $b): int {
    return $a + $b;
}

// Variadic functions (variable number of args)
function sum(...$nums) {
    return array_sum($nums);
}
echo sum(1, 2, 3, 4);         // 10
?>
Exercise — your turn

Write the three function bodies (one uses a default parameter, one is variadic), then double every number with array_map and an arrow function.

<?php
function maxOf(int $a, int $b): int {
    // TODO: return the larger of $a and $b
}

function greet(string $name = "World"): string {
    // TODO: return "Hello, $name!"
}

function total(int ...$nums): int {
    // TODO: add up $nums and return the sum
}

echo "maxOf(4, 9) = " . maxOf(4, 9) . "\n";
echo greet() . "\n";
echo "Sum: " . total(1, 2, 3, 4, 5) . "\n";

$doubled = [1, 2, 3];   // TODO: array_map(fn($n) => ..., [1, 2, 3])
echo "Doubled: " . implode(", ", $doubled) . "\n";
Show solution
<?php
function maxOf(int $a, int $b): int {
    return $a > $b ? $a : $b;
}

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

function total(int ...$nums): int {
    $sum = 0;
    foreach ($nums as $n) {
        $sum += $n;
    }
    return $sum;
}

echo "maxOf(4, 9) = " . maxOf(4, 9) . "\n";
echo greet() . "\n";
echo "Sum: " . total(1, 2, 3, 4, 5) . "\n";

$doubled = array_map(fn($n) => $n * 2, [1, 2, 3]);
echo "Doubled: " . implode(", ", $doubled) . "\n";
Output
maxOf(4, 9) = 9
Hello, World!
Sum: 15
Doubled: 2, 4, 6
Explain it simply

Why can't a PHP function see a variable defined outside it? Why is that a good rule?

Reveal a plain-language answer

Each function works in its own sealed room: it only has what you hand it through the door (its parameters) and only gives back what it returns. If functions could reach out and change any variable in the script, a function on one page could silently break code somewhere else, and you'd have to read the whole program to trust any line. Sealed rooms mean you can understand a function just by reading it.

10. Forms & User Input

The web's basic way of collecting information from a visitor is the HTML form. When the visitor submits it, the browser sends the form's fields to the URL in its action attribute, using one of two methods. With GET, the data is appended to the URL as a query string (?q=php&page=2). It's visible, bookmarkable, and suited to searches and filters. With POST, the data travels in the body of the request. POST is used for anything that changes something, such as signing up, saving, or buying, or for anything sensitive.

PHP makes the submitted data available in superglobals, special arrays that are available everywhere in every script. $_GET holds query-string values, and $_POST holds posted form fields, keyed by each input's name attribute. $_SERVER holds request details such as $_SERVER["REQUEST_METHOD"]. Every value arrives as a string (or is missing entirely), so a script must read each field defensively with ??, then validate it: check that it's present, the right length, and the right kind of value, for example with filter_var($x, FILTER_VALIDATE_INT).

The golden rule of web programming is never trust user input. Anything a visitor submits could be malicious. If you print it back into a page unchanged, a visitor can inject their own HTML or JavaScript, an attack called cross-site scripting (XSS). The defence is to escape output with htmlspecialchars(), which turns < into &lt; so the browser displays it as text instead of running it. Likewise, never paste input directly into an SQL query; use prepared statements, which keep data separate from the query and prevent SQL injection.

GET vs POST
Data in the URL (for reading and searching) / data in the request body (for changes).
Superglobal
A built-in array available everywhere, such as $_GET, $_POST, $_SERVER.
XSS
Cross-site scripting: injecting code into a page through unescaped user input.
<!-- form.html -->
<form method="post" action="process.php">
    <input type="text" name="username">
    <input type="submit" value="Submit">
</form>
<!-- process.php -->
<?php
if ($_SERVER["REQUEST_METHOD"] === "POST") {
    // Always sanitize user input!
    $username = htmlspecialchars(trim($_POST["username"] ?? ""));

    if (empty($username)) {
        echo "Username is required.";
    } else {
        echo "Welcome, $username!";
    }
}
?>
Always sanitize and validate user input. Use htmlspecialchars() to prevent XSS, and never trust $_GET / $_POST values directly in SQL queries — use prepared statements instead.
Exercise — your turn

On a real server you'd call handle($_POST); here the “form” is passed in as an array so it runs from the command line. Escape the username, validate the age as an integer, and return the right message. Notice what the first submission's sneaky <b> tags turn into.

<?php
function handle(array $post): string {
    $username = trim($post["username"] ?? "");   // TODO: also escape with htmlspecialchars
    $age = $post["age"] ?? "";                  // TODO: validate with filter_var(..., FILTER_VALIDATE_INT)

    // TODO: return "Error: Username is required." if $username is empty
    // TODO: return "Error: Age must be a whole number." if $age is false
    return "Welcome, $username! Age $age.";
}

// On a real server you would call handle($_POST).
echo handle(["username" => "  <b>Ada</b>  ", "age" => "36"]), "\n";
echo handle(["username" => "Ada", "age" => "abc"]), "\n";
echo handle(["age" => "36"]), "\n";
Show solution
<?php
function handle(array $post): string {
    $username = htmlspecialchars(trim($post["username"] ?? ""));
    $age = filter_var($post["age"] ?? "", FILTER_VALIDATE_INT);

    if ($username === "") {
        return "Error: Username is required.";
    }
    if ($age === false) {
        return "Error: Age must be a whole number.";
    }
    return "Welcome, $username! Age $age.";
}

// On a real server you would call handle($_POST).
echo handle(["username" => "  <b>Ada</b>  ", "age" => "36"]), "\n";
echo handle(["username" => "Ada", "age" => "abc"]), "\n";
echo handle(["age" => "36"]), "\n";
Output
Welcome, &lt;b&gt;Ada&lt;/b&gt;! Age 36.
Error: Age must be a whole number.
Error: Username is required.
Explain it simply

Why must you run user input through htmlspecialchars before printing it? Explain it with a sign-maker.

Reveal a plain-language answer

Imagine a shop that prints whatever customers write onto signs. If a customer writes instructions for the printer itself, such as “also print a fake price list”, a careless sign-maker obeys them. htmlspecialchars is the careful sign-maker: it prints the customer's words exactly as letters, so <script> appears on the sign as harmless text instead of being followed as an instruction by the browser.

11. Intro to OOP

Object-oriented programming (OOP) organises code around objects, bundles of data together with the functions that work on that data. A class is the blueprint, and an object is an instance created from it with new. Most modern PHP, including frameworks such as Laravel and Symfony, is written this way. A class contains properties (its data) and methods (its functions), and inside a method $this refers to the current object. Members are reached with the arrow operator: $acct->deposit(100).

The special method __construct is the constructor: it runs automatically when you call new and sets up the object's starting state. PHP 8's constructor property promotion lets you declare and assign a property straight from the parameter list, as in public function __construct(public readonly string $owner) {}. readonly (PHP 8.1) allows a property to be set once and never changed. Other magic methods hook into built-in behaviour. __toString, for example, controls what an object looks like when it is echoed.

Visibility controls who may touch each member: public members are open to all code, protected ones to the class and its subclasses, and private ones to the class alone. Keeping data private and exposing only deliberate methods is called encapsulation. A bank account shouldn't let outside code set its balance to any number, only deposit and withdraw under the rules. Classes can also build on one another with extends (inheritance), and promise capabilities with implements (interfaces).

$this
Inside a method, the object the method was called on.
Constructor
__construct, run by new to initialise a new object.
Encapsulation
Keeping an object's data private and exposing only controlled methods.
<?php
class Animal {
    // Properties
    public string $name;
    protected int $age;
    private string $secret = "hidden";

    // Constructor
    public function __construct(string $name, int $age) {
        $this->name = $name;
        $this->age  = $age;
    }

    // Method
    public function describe(): string {
        return "{$this->name} is {$this->age} years old.";
    }
}

// Inheritance
class Dog extends Animal {
    public function speak(): string {
        return $this->name . " says: Woof!";
    }
}

$dog = new Dog("Rex", 4);
echo $dog->describe();          // Rex is 4 years old.
echo $dog->speak();             // Rex says: Woof!
?>
VisibilityAccessible from
publicAnywhere
protectedClass itself and subclasses
privateClass itself only
Exercise — your turn

Finish BankAccount: $balance is private, so the only way to change it is through deposit and withdraw, and withdraw must refuse (return false) if the money isn't there.

<?php
class BankAccount {
    private int $balance = 0;

    public function __construct(public readonly string $owner) {
    }

    public function deposit(int $amount): void {
        // TODO: add $amount to the balance
    }

    public function withdraw(int $amount): bool {
        // TODO: return false if $amount is more than the balance,
        //       otherwise subtract it and return true
    }

    public function __toString(): string {
        return "{$this->owner}: {$this->balance}";
    }
}

$acct = new BankAccount("Ada");
$acct->deposit(100);
echo $acct, "\n";
echo "Withdraw 250? " . ($acct->withdraw(250) ? "yes" : "no") . "\n";
echo "Withdraw 40? " . ($acct->withdraw(40) ? "yes" : "no") . "\n";
echo $acct, "\n";
Show solution
<?php
class BankAccount {
    private int $balance = 0;

    public function __construct(public readonly string $owner) {
    }

    public function deposit(int $amount): void {
        $this->balance += $amount;
    }

    public function withdraw(int $amount): bool {
        if ($amount > $this->balance) {
            return false;
        }
        $this->balance -= $amount;
        return true;
    }

    public function __toString(): string {
        return "{$this->owner}: {$this->balance}";
    }
}

$acct = new BankAccount("Ada");
$acct->deposit(100);
echo $acct, "\n";
echo "Withdraw 250? " . ($acct->withdraw(250) ? "yes" : "no") . "\n";
echo "Withdraw 40? " . ($acct->withdraw(40) ? "yes" : "no") . "\n";
echo $acct, "\n";
Output
Ada: 100
Withdraw 250? no
Withdraw 40? yes
Ada: 60
Explain it simply

What stops outside code from writing $acct->balance = 1000000;? Why is that a feature?

Reveal a plain-language answer

private does: PHP refuses to let any code outside the class touch that property, and throws an error if it tries. That's the point. The balance can only change through deposit and withdraw, which enforce the rules (no overdrafts). A bank teller checks the rules for you; nobody gets to reach behind the counter and rewrite the ledger.