Skip to main content

🔤 Lesson 10: Strings & String Functions

Strings are everywhere in web development — URLs, form data, HTML output, database queries, file paths, emails. PHP has one of the richest string function libraries of any language, and mastering it will make you dramatically more productive.

🎯 Learning Objectives

By the end of this lesson, you will be able to:

  • Measure, search, and extract substrings with strlen, strpos, and substr
  • Replace and transform text with str_replace, case functions, and trim
  • Convert between strings and arrays with explode and implode
  • Format output with sprintf and number_format
  • Write multiline strings with heredoc and nowdoc syntax
  • Use basic regular expressions with preg_match and preg_replace

Estimated Time: 45 minutes

Prerequisites: Lesson 8 (arrays), Lesson 9 (array functions)

📑 In This Lesson

String Basics Review

A quick refresher on what we covered in Lesson 3 about strings, plus a few new details.

Single vs. Double Quotes

<?php
$name = "Alice";

// Double quotes — variables ARE interpolated
echo "Hello, $name!\n";        // Hello, Alice!
echo "Tab:\there\n";            // Tab:    here  (escape sequences work)

// Single quotes — variables are NOT interpolated
echo 'Hello, $name!\n';        // Hello, $name!\n  (literal text)
echo 'Only two escapes: \' and \\';  // Apostrophe and backslash

// Complex interpolation with curly braces
$user = ["name" => "Alice", "role" => "admin"];
echo "Welcome, {$user['name']}! Role: {$user['role']}\n";
echo "She has {$count} item" . ($count !== 1 ? "s" : "") . "\n";

String Concatenation

<?php
// Dot operator (.) joins strings
$first = "Hello";
$second = "World";
$greeting = $first . ", " . $second . "!";
echo $greeting; // "Hello, World!"

// Concatenation assignment (.=)
$message = "Step 1: Open the file. ";
$message .= "Step 2: Read the data. ";
$message .= "Step 3: Process results.";
echo $message;

Escape Sequences (Double Quotes Only)

Sequence Meaning Example
\n Newline "Line 1\nLine 2"
\t Tab "Col1\tCol2"
\\ Literal backslash "C:\\path\\file"
\$ Literal dollar sign "Price: \$9.99"
\" Literal double quote "She said \"hi\""

Strings Are Byte Sequences

In PHP, each character in a string is one byte. This works fine for ASCII text but can cause issues with multibyte characters (like emoji or accented letters). For multibyte-safe operations, use the mb_* functions:

<?php
$text = "Café";
echo strlen($text);      // 5 (the é is 2 bytes in UTF-8!)
echo mb_strlen($text);   // 4 (correct character count)

$emoji = "Hello 🌍";
echo strlen($emoji);     // 10 (🌍 is 4 bytes)
echo mb_strlen($emoji);  // 7  (correct character count)

✅ Rule of Thumb

If your application handles user input (which could contain emoji, accented characters, or non-Latin scripts), prefer mb_strlen(), mb_substr(), mb_strpos(), etc. For purely ASCII data (like processing CSV files with English data), the standard functions are fine and slightly faster.

Measuring & Searching

strlen() — String Length

<?php
echo strlen("Hello");        // 5
echo strlen("");              // 0
echo strlen("Hello World");  // 11 (space counts!)
echo strlen("   ");          // 3 (spaces are characters)

// Common use: validation
$password = "abc123";
if (strlen($password) < 8) {
    echo "Password must be at least 8 characters!\n";
}

strpos() — Find Position of Substring

<?php
$text = "The quick brown fox jumps over the lazy dog";

// Find first occurrence
echo strpos($text, "fox");     // 16
echo strpos($text, "the");     // 31 (case-sensitive! "the" ≠ "The")
echo strpos($text, "cat");     // false (not found)

// Start searching from a position
echo strpos($text, "the", 10); // 31 (start search from index 10)

// Case-insensitive search
echo stripos($text, "the");    // 0 (finds "The" at position 0)

⚠️ strpos Returns 0 or false — Use Strict Comparison!

This is one of PHP's most common gotchas. strpos() returns 0 when the substring is at the beginning, and 0 is falsy. Always use ===:

$text = "Hello World";

// BAD — 0 is falsy, so this incorrectly says "not found"!
if (strpos($text, "Hello") == false) {
    echo "Not found!"; // WRONG — this prints!
}

// GOOD — strict comparison
if (strpos($text, "Hello") === false) {
    echo "Not found!";
} else {
    echo "Found!"; // Correct!
}

// BEST (PHP 8+) — use str_contains instead
if (str_contains($text, "Hello")) {
    echo "Found!"; // Clean and clear
}

PHP 8 String Search Functions

PHP 8 introduced three cleaner alternatives to strpos() for common checks:

<?php
$url = "https://www.example.com/products/widget";

// str_contains — does the string contain a substring?
echo str_contains($url, "example");    // true
echo str_contains($url, "Example");    // false (case-sensitive)

// str_starts_with — does it start with...?
echo str_starts_with($url, "https");   // true
echo str_starts_with($url, "http://"); // false

// str_ends_with — does it end with...?
echo str_ends_with($url, "widget");    // true
echo str_ends_with($url, ".html");     // false

// Practical examples
$filename = "report_2026.pdf";
if (str_ends_with($filename, ".pdf")) {
    echo "It's a PDF!\n";
}

$email = "alice@example.com";
if (str_contains($email, "@")) {
    echo "Looks like an email\n";
}

✅ Prefer PHP 8 Functions

Use str_contains(), str_starts_with(), and str_ends_with() whenever possible. They're clearer than strpos() !== false and avoid the zero-vs-false trap entirely.

strrpos() — Find Last Occurrence

<?php
$path = "/home/user/documents/report.pdf";

// Find last occurrence of "/"
$lastSlash = strrpos($path, "/");
echo $lastSlash; // 20

// Useful for extracting filenames
$filename = substr($path, $lastSlash + 1);
echo $filename; // "report.pdf"

// Find last occurrence of "." for file extension
$lastDot = strrpos($path, ".");
$extension = substr($path, $lastDot + 1);
echo $extension; // "pdf"

substr_count() — Count Occurrences

<?php
$text = "she sells seashells by the seashore";

echo substr_count($text, "sea");  // 2 ("seashells" and "seashore")
echo substr_count($text, "she");  // 3 ("she", "seashells", "seashore")
echo substr_count($text, "the");  // 1

Extracting Substrings

substr() — Extract a Portion

<?php
$text = "Hello, World!";

// From position 7 to end
echo substr($text, 7);        // "World!"

// From position 7, take 5 characters
echo substr($text, 7, 5);     // "World"

// From position 0, take 5 characters
echo substr($text, 0, 5);     // "Hello"

// Negative offset — count from end
echo substr($text, -6);       // "orld!"
echo substr($text, -6, 4);    // "orld"

// Negative length — stop N characters before end
echo substr($text, 0, -1);    // "Hello, World"  (removes last char)
echo substr($text, 7, -1);    // "World"         (from 7, stop before last)

Practical Extraction Patterns

<?php
// Extract file extension
$file = "vacation_photo.jpg";
$ext = substr($file, strrpos($file, ".") + 1);
echo $ext; // "jpg"

// Or use pathinfo (the better way for file paths)
$info = pathinfo($file);
echo $info["extension"]; // "jpg"
echo $info["filename"];  // "vacation_photo"

// Truncate long text with ellipsis
function truncate(string $text, int $maxLength = 50): string {
    if (strlen($text) <= $maxLength) {
        return $text;
    }
    return substr($text, 0, $maxLength - 3) . "...";
}

echo truncate("This is a very long sentence that goes on and on", 30);
// "This is a very long senten..."

// Extract domain from email
$email = "alice@example.com";
$domain = substr($email, strpos($email, "@") + 1);
echo $domain; // "example.com"

substr_replace() — Replace a Portion

<?php
$phone = "2025551234";

// Insert dashes to format phone number
$formatted = substr_replace($phone, "-", 3, 0);  // Insert after 3
$formatted = substr_replace($formatted, "-", 7, 0); // Insert after 7
echo $formatted; // "202-555-1234"

// Mask a credit card number
$cc = "4532015112830366";
$masked = substr_replace($cc, str_repeat("*", 12), 0, 12);
echo $masked; // "************0366"

// Or show first and last 4
$masked = substr($cc, 0, 4) . str_repeat("*", 8) . substr($cc, -4);
echo $masked; // "4532********0366"

Replacing & Transforming

str_replace() — Find and Replace

<?php
$text = "I love cats. Cats are amazing. My cat is fluffy.";

// Simple replacement (case-sensitive)
echo str_replace("cat", "dog", $text);
// "I love dogs. Cats are amazing. My dog is fluffy."
// Notice: "Cats" wasn't replaced (capital C)

// Case-insensitive replacement
echo str_ireplace("cat", "dog", $text);
// "I love dogs. dogs are amazing. My dog is fluffy."

// Replace multiple values at once
$clean = str_replace(
    ["<br>", "<br/>", "<br />"],
    "\n",
    "Line 1<br>Line 2<br/>Line 3<br />Line 4"
);
// "Line 1\nLine 2\nLine 3\nLine 4"

// Map replacements (parallel arrays)
$template = "Dear {NAME}, your order #{ORDER} ships on {DATE}.";
$message = str_replace(
    ["{NAME}", "{ORDER}", "{DATE}"],
    ["Alice", "1042", "April 20, 2026"],
    $template
);
echo $message;
// "Dear Alice, your order #1042 ships on April 20, 2026."

// Count replacements made
$result = str_replace("the", "THE", $text, $count);
echo "Made $count replacements"; // Made 0 replacements (no "the" in original)

Case Conversion

<?php
$text = "Hello World";

echo strtolower($text);   // "hello world"
echo strtoupper($text);   // "HELLO WORLD"
echo ucfirst("hello");    // "Hello"     (capitalize first letter)
echo lcfirst("Hello");    // "hello"     (lowercase first letter)
echo ucwords("hello world from php");  // "Hello World From Php"

// ucwords with custom delimiters
echo ucwords("hello-world_from-php", "-_");
// "Hello-World_From-Php"

// Convert to title case (handle common words)
function title_case(string $text): string {
    $small = ["a", "an", "the", "and", "but", "or", "for", "in", "on", "at", "to", "of"];
    $words = explode(" ", strtolower($text));
    foreach ($words as $i => &$word) {
        if ($i === 0 || !in_array($word, $small)) {
            $word = ucfirst($word);
        }
    }
    return implode(" ", $words);
}

echo title_case("the lord of the rings"); // "The Lord of the Rings"

Trimming Whitespace

<?php
$input = "   Hello, World!   ";

echo trim($input);   // "Hello, World!"  (both sides)
echo ltrim($input);  // "Hello, World!   " (left only)
echo rtrim($input);  // "   Hello, World!" (right only)

// Trim specific characters
echo trim("***Hello***", "*");     // "Hello"
echo trim("...Hello...", ".");     // "Hello"
echo ltrim("/path/to/file", "/");  // "path/to/file"
echo rtrim("Hello!!!", "!");       // "Hello"

// Common use: clean form input
$email = trim($_POST["email"] ?? "");
$name  = trim($_POST["name"] ?? "");

Padding Strings

<?php
// str_pad — pad to a minimum length
echo str_pad("42", 5, "0", STR_PAD_LEFT);   // "00042"
echo str_pad("Hi", 10, ".");                  // "Hi........"
echo str_pad("Hi", 10, "-", STR_PAD_BOTH);   // "----Hi----"

// Practical: format an invoice line
function format_line(string $item, float $price): string {
    $left = str_pad($item, 30, ".");
    $right = str_pad(number_format($price, 2), 10, " ", STR_PAD_LEFT);
    return $left . $right;
}

echo format_line("Widget (x3)", 38.97) . "\n";
echo format_line("Gadget (x1)", 49.99) . "\n";
// Widget (x3)....................     38.97
// Gadget (x1)....................     49.99

Repeating Strings

<?php
echo str_repeat("=", 40) . "\n";  // ========================================
echo str_repeat("- ", 20) . "\n";  // - - - - - - - - - - - - - - - - - - - - 
echo str_repeat("Ha", 3);          // HaHaHa

// Practical: create a visual bar chart
function bar(string $label, int $value, int $max = 50): string {
    $width = (int)($value / $max * 30);
    return sprintf("%-10s |%s| %d", $label, str_repeat("█", $width), $value);
}

echo bar("PHP", 45) . "\n";
echo bar("Python", 38) . "\n";
echo bar("JS", 50) . "\n";

Splitting & Joining

explode() — String → Array

<?php
// Basic split
$csv = "Alice,Bob,Carol,Dave";
$names = explode(",", $csv);
// ["Alice", "Bob", "Carol", "Dave"]

// Split by any delimiter
$path = "/home/user/documents/file.txt";
$parts = explode("/", $path);
// ["", "home", "user", "documents", "file.txt"]

// Limit the number of splits
$log = "2026-04-17 14:30:00 ERROR Something went wrong badly";
$parts = explode(" ", $log, 3); // Split into at most 3 pieces
// ["2026-04-17", "14:30:00", "ERROR Something went wrong badly"]

// Split and destructure
[$date, $time, $message] = explode(" ", $log, 3);
echo "Date: $date, Time: $time, Message: $message\n";

// Parse key=value pairs
$queryString = "name=Alice&age=30&city=Portland";
$pairs = explode("&", $queryString);
$params = [];
foreach ($pairs as $pair) {
    [$key, $value] = explode("=", $pair);
    $params[$key] = $value;
}
// ["name" => "Alice", "age" => "30", "city" => "Portland"]

implode() — Array → String

<?php
$words = ["Hello", "World", "from", "PHP"];
echo implode(" ", $words);   // "Hello World from PHP"
echo implode(", ", $words);  // "Hello, World, from, PHP"
echo implode("", $words);    // "HelloWorldfromPHP"

// Build a CSV line
$row = ["Widget", "12.99", "50", "Tools"];
echo implode(",", $row); // "Widget,12.99,50,Tools"

// Build an HTML list
$items = ["Home", "About", "Contact"];
$html = "<li>" . implode("</li><li>", $items) . "</li>";
// "<li>Home</li><li>About</li><li>Contact</li>"

// Build a SQL IN clause
$ids = [1, 5, 12, 23];
$in = implode(",", $ids);
$sql = "SELECT * FROM users WHERE id IN ($in)";
// ⚠️ For real queries, use prepared statements! This is just to show implode.

str_split() — Split into Characters or Chunks

<?php
// Split into individual characters
$chars = str_split("Hello");
// ["H", "e", "l", "l", "o"]

// Split into chunks of N characters
$chunks = str_split("ABCDEFGHIJ", 3);
// ["ABC", "DEF", "GHI", "J"]

// Practical: format a serial number
$serial = "A1B2C3D4E5";
echo implode("-", str_split($serial, 4));
// "A1B2-C3D4-E5"

wordwrap() — Wrap Long Text

<?php
$text = "PHP is a popular general-purpose scripting language that powers everything from blogs to enterprise applications.";

// Wrap at 40 characters
echo wordwrap($text, 40, "\n");
/* Output:
PHP is a popular general-purpose
scripting language that powers
everything from blogs to enterprise
applications.
*/

// Force-cut long words
$long = "Supercalifragilisticexpialidocious is a very long word.";
echo wordwrap($long, 15, "\n", true);

Formatting Output

sprintf() — Formatted Strings

sprintf() lets you build strings with precise control over how values are formatted. It uses format specifiers as placeholders:

Specifier Type Example Output
%s String sprintf("Hi, %s", "Alice") Hi, Alice
%d Integer sprintf("Count: %d", 42) Count: 42
%f Float sprintf("Pi: %f", 3.14159) Pi: 3.141590
%.2f Float (2 decimals) sprintf("$%.2f", 9.5) $9.50
%05d Zero-padded int sprintf("%05d", 42) 00042
%-20s Left-aligned string sprintf("%-20s|", "hi") hi |
%10s Right-aligned string sprintf("%10s|", "hi") hi|
%% Literal % sprintf("%d%%", 85) 85%
<?php
// Basic formatting
echo sprintf("Hello, %s! You are %d years old.\n", "Alice", 30);
// "Hello, Alice! You are 30 years old."

// Price formatting
echo sprintf("Price: $%8.2f\n", 1234.5);
// "Price: $ 1234.50"

// Table-style output
$products = [
    ["Widget", 12.99, 150],
    ["Gadget", 49.99, 30],
    ["Doohickey", 8.50, 200],
];

echo sprintf("%-15s %10s %8s\n", "Product", "Price", "Stock");
echo str_repeat("-", 35) . "\n";
foreach ($products as [$name, $price, $stock]) {
    echo sprintf("%-15s %10.2f %8d\n", $name, $price, $stock);
}
/* Output:
Product              Price    Stock
-----------------------------------
Widget               12.99      150
Gadget               49.99       30
Doohickey             8.50      200
*/

// printf — same as sprintf but prints directly
printf("Order #%05d: $%.2f\n", 42, 99.50);
// "Order #00042: $99.50"

number_format() — Format Numbers for Display

<?php
echo number_format(1234567.891);        // "1,234,568"       (rounded, no decimals)
echo number_format(1234567.891, 2);     // "1,234,567.89"    (2 decimal places)
echo number_format(1234567.891, 2, ".", ","); // "1,234,567.89" (explicit separators)
echo number_format(1234567.891, 2, ",", "."); // "1.234.567,89" (European format)
echo number_format(0.5, 2);            // "0.50"

// Practical use
$price = 49999.99;
echo "Total: $" . number_format($price, 2); // "Total: $49,999.99"

Heredoc & Nowdoc

When you need multiline strings — especially for HTML templates, SQL queries, or email bodies — heredoc and nowdoc syntax keeps your code clean.

Heredoc (Like Double Quotes)

Variables are interpolated, escape sequences work:

<?php
$name = "Alice";
$role = "Admin";

$html = <<<HTML
<div class="user-card">
    <h2>{$name}</h2>
    <p>Role: {$role}</p>
    <p>Welcome to the dashboard!</p>
</div>
HTML;

echo $html;

Nowdoc (Like Single Quotes)

No interpolation — everything is literal text. Note the single quotes around the identifier:

<?php
$code = <<<'CODE'
<?php
$name = "World";
echo "Hello, $name!";
// This $name won't be interpolated
?>
CODE;

echo $code;
// Outputs the PHP code literally, with $name as text

Practical Use Cases

<?php
// Email template with heredoc
$to = "alice@example.com";
$orderNum = 1042;
$total = 89.97;

$body = <<<EMAIL
Hi {$to},

Your order #{$orderNum} has been confirmed!

Order Total: \${$total}
Estimated Delivery: 3-5 business days

Thank you for your purchase!

Best regards,
The Store Team
EMAIL;

// SQL query with heredoc (for readability — still use prepared statements!)
$table = "products";
$sql = <<<SQL
SELECT
    p.name,
    p.price,
    c.name AS category
FROM {$table} p
JOIN categories c ON p.category_id = c.id
WHERE p.price > 10
ORDER BY p.price DESC
LIMIT 20
SQL;

✅ When to Use Heredoc/Nowdoc

Heredoc is great for HTML templates and email bodies where you need variable interpolation. Nowdoc is ideal for code examples, raw text, or anything where you want zero interpolation. Both are cleaner than concatenating lots of strings with dots.

Regular Expressions Basics

Regular expressions (regex) are patterns that describe text. They're powerful for validation, searching, and complex replacements. PHP uses PCRE (Perl Compatible Regular Expressions).

📖 Definition

Regular Expression: A pattern string that describes a set of matching strings. In PHP, regex patterns are delimited by forward slashes: /pattern/flags.

Essential Pattern Characters

Pattern Meaning Example Matches
. Any character (except newline) /h.t/ hat, hit, hot, h9t
\d Any digit (0-9) /\d{3}/ 123, 456, 007
\w Word character (letter, digit, _) /\w+/ hello, user_1
\s Whitespace /\s+/ spaces, tabs, newlines
^ Start of string /^Hello/ "Hello World"
$ End of string /\.php$/ "index.php"
+ One or more /\d+/ 1, 42, 9999
* Zero or more /go*d/ gd, god, good, goood
? Zero or one /colou?r/ color, colour
{n} Exactly n times /\d{4}/ 2026, 1234
{n,m} Between n and m times /\d{2,4}/ 42, 123, 2026
[abc] Character class /[aeiou]/ Any vowel
[^abc] Not these characters /[^0-9]/ Any non-digit
(group) Capture group /(\d+)-(\d+)/ Captures "123" and "456" from "123-456"
| OR /cat|dog/ cat or dog

preg_match() — Test for a Match

<?php
// Returns 1 if pattern matches, 0 if not
$email = "alice@example.com";
if (preg_match('/^[\w.+-]+@[\w-]+\.[\w.]+$/', $email)) {
    echo "Valid email format!\n";
}

// Capture groups
$date = "2026-04-17";
if (preg_match('/^(\d{4})-(\d{2})-(\d{2})$/', $date, $matches)) {
    echo "Full match: {$matches[0]}\n"; // "2026-04-17"
    echo "Year: {$matches[1]}\n";       // "2026"
    echo "Month: {$matches[2]}\n";      // "04"
    echo "Day: {$matches[3]}\n";        // "17"
}

// Named capture groups
$url = "https://example.com:8080/path";
$pattern = '/^(?P<scheme>https?):\/\/(?P<host>[\w.]+)(:(?P<port>\d+))?/';
if (preg_match($pattern, $url, $matches)) {
    echo "Scheme: {$matches['scheme']}\n"; // "https"
    echo "Host: {$matches['host']}\n";     // "example.com"
    echo "Port: {$matches['port']}\n";     // "8080"
}

preg_match_all() — Find All Matches

<?php
$text = "Call 555-1234 or 555-5678. Fax: 555-9999.";

// Find all phone numbers
$count = preg_match_all('/\d{3}-\d{4}/', $text, $matches);
echo "Found $count numbers:\n";
print_r($matches[0]); // ["555-1234", "555-5678", "555-9999"]

// Extract all hashtags
$post = "Loving the #sunset today! #photography #nature #beautiful";
preg_match_all('/#(\w+)/', $post, $matches);
print_r($matches[1]); // ["sunset", "photography", "nature", "beautiful"]

preg_replace() — Search and Replace with Regex

<?php
// Remove extra whitespace
$messy = "Hello    World   from     PHP";
$clean = preg_replace('/\s+/', ' ', $messy);
echo $clean; // "Hello World from PHP"

// Remove non-alphanumeric characters
$input = "Hello, World! @#$ 123";
$clean = preg_replace('/[^a-zA-Z0-9\s]/', '', $input);
echo $clean; // "Hello World  123"

// Format a phone number
$phone = "2025551234";
$formatted = preg_replace('/(\d{3})(\d{3})(\d{4})/', '($1) $2-$3', $phone);
echo $formatted; // "(202) 555-1234"

// Censor bad words
$text = "This is damn annoying and hell of a mess";
$censored = preg_replace('/\b(damn|hell)\b/i', '****', $text);
echo $censored; // "This is **** annoying and **** of a mess"

// Convert snake_case to camelCase
$snake = "get_user_by_id";
$camel = preg_replace_callback('/_([a-z])/', fn($m) => strtoupper($m[1]), $snake);
echo $camel; // "getUserById"

preg_split() — Split with Regex

<?php
// Split on any whitespace (spaces, tabs, multiple spaces)
$text = "Hello   World\tfrom  PHP";
$words = preg_split('/\s+/', $text);
// ["Hello", "World", "from", "PHP"]

// Split on multiple delimiters
$data = "one,two;three|four.five";
$parts = preg_split('/[,;|.]/', $data);
// ["one", "two", "three", "four", "five"]

⚠️ Regex Performance

Regular expressions are powerful but slower than dedicated string functions. If a simple str_contains(), str_replace(), or explode() can do the job, prefer those. Use regex when you need pattern matching that simple functions can't handle — like validating formats, capturing groups, or matching variable text.

Common Regex Patterns

Pattern Validates
/^\d{5}(-\d{4})?$/ US ZIP code (12345 or 12345-6789)
/^\d{3}-\d{3}-\d{4}$/ US phone (555-555-1234)
/^#?[0-9a-fA-F]{6}$/ Hex color (#FF00AA or FF00AA)
/^https?:\/\/.+/ URL starts with http(s)://
/^\d{4}-\d{2}-\d{2}$/ Date format (YYYY-MM-DD)
/^[a-zA-Z0-9_]{3,20}$/ Username (3-20 alphanumeric + underscore)
💡 Tip: For email validation, prefer filter_var($email, FILTER_VALIDATE_EMAIL) over regex — it handles the full RFC spec. We'll cover this in Lesson 12 (Input Validation).

Hands-On Exercises

🏋️ Exercise 1: Text Analyzer

Objective: Build a function that analyzes a block of text and returns statistics.

Instructions:

  1. Write analyze_text(string $text): array that returns:
    • char_count — total characters
    • word_count — total words
    • sentence_count — total sentences (count ., !, ?)
    • avg_word_length — average word length
    • longest_word — the longest word
    • unique_words — count of unique words (case-insensitive)
  2. Test it with a paragraph of text
💡 Hint

Use preg_split('/\s+/', $text) to split into words. Use preg_match_all('/[.!?]/', $text) to count sentences. Use array_map with strlen to get word lengths.

✅ Solution
<?php
function analyze_text(string $text): array {
    // Clean and split into words
    $text = trim($text);
    $words = preg_split('/\s+/', $text);
    $wordCount = count($words);

    // Sentence count
    $sentenceCount = preg_match_all('/[.!?]+/', $text);

    // Word lengths
    $cleanWords = array_map(fn($w) => preg_replace('/[^a-zA-Z]/', '', $w), $words);
    $cleanWords = array_filter($cleanWords, fn($w) => strlen($w) > 0);
    $lengths = array_map('strlen', $cleanWords);
    $avgLength = count($lengths) > 0 ? array_sum($lengths) / count($lengths) : 0;

    // Longest word
    $longest = array_reduce($cleanWords,
        fn($carry, $w) => strlen($w) > strlen($carry) ? $w : $carry, "");

    // Unique words (case-insensitive)
    $lowerWords = array_map('strtolower', $cleanWords);
    $uniqueCount = count(array_unique($lowerWords));

    return [
        "char_count"      => strlen($text),
        "word_count"       => $wordCount,
        "sentence_count"   => $sentenceCount,
        "avg_word_length"  => round($avgLength, 1),
        "longest_word"     => $longest,
        "unique_words"     => $uniqueCount,
    ];
}

// Test it
$sample = "PHP is a popular scripting language. It powers millions of websites! 
Do you know PHP? PHP is great for web development. Learning PHP is fun and rewarding.";

$stats = analyze_text($sample);
echo "📊 Text Analysis\n";
echo str_repeat("=", 30) . "\n";
foreach ($stats as $key => $value) {
    echo sprintf("  %-18s %s\n", str_replace("_", " ", ucfirst($key)) . ":", $value);
}

🏋️ Exercise 2: Template Engine

Objective: Build a simple template engine that replaces placeholders with values.

Instructions:

  1. Write render_template(string $template, array $data): string
  2. Replace {{key}} placeholders with values from the $data array
  3. Handle missing keys gracefully (replace with [MISSING: key])
  4. Support a {{key|upper}} filter for uppercase and {{key|lower}} for lowercase
  5. Test with an email template
💡 Hint

Use preg_replace_callback with the pattern /\{\{(\w+)(?:\|(\w+))?\}\}/ to match placeholders. The first capture group is the key name, the optional second is the filter.

✅ Solution
<?php
function render_template(string $template, array $data): string {
    return preg_replace_callback(
        '/\{\{(\w+)(?:\|(\w+))?\}\}/',
        function($matches) use ($data) {
            $key = $matches[1];
            $filter = $matches[2] ?? null;

            // Get value or show missing
            if (!array_key_exists($key, $data)) {
                return "[MISSING: $key]";
            }

            $value = (string) $data[$key];

            // Apply filter
            return match ($filter) {
                'upper' => strtoupper($value),
                'lower' => strtolower($value),
                'ucfirst' => ucfirst($value),
                'trim' => trim($value),
                null => $value,
                default => $value,
            };
        },
        $template
    );
}

// Test it
$template = <<<TPL
Dear {{name|ucfirst}},

Thank you for your order #{{order_id}}!

Items: {{items}}
Total: \${{total}}
Status: {{status|upper}}

Shipping to: {{address}}
Expected delivery: {{delivery_date}}

Best regards,
{{company|upper}} Team
TPL;

$data = [
    "name"          => "alice",
    "order_id"      => "1042",
    "items"         => "Widget x3, Gadget x1",
    "total"         => "88.96",
    "status"        => "confirmed",
    "address"       => "123 Main St, Portland, OR",
    "company"       => "Acme Widgets",
];

echo render_template($template, $data);

🏋️ Exercise 3: URL Slug Generator

Objective: Build a function that converts titles into URL-friendly slugs.

Instructions:

  1. Write slugify(string $text): string
  2. Convert to lowercase
  3. Replace spaces and special characters with hyphens
  4. Remove consecutive hyphens
  5. Trim hyphens from start and end

Examples:

slugify("Hello World!")          → "hello-world"
slugify("PHP 8.3: What's New?")  → "php-8-3-whats-new"
slugify("  Extra   Spaces  ")    → "extra-spaces"
slugify("CamelCase & snake_case") → "camelcase-snake-case"
✅ Solution
<?php
function slugify(string $text): string {
    $text = strtolower(trim($text));              // Lowercase & trim
    $text = preg_replace('/[^a-z0-9\s-]/', '', $text); // Remove special chars
    $text = preg_replace('/[\s_]+/', '-', $text);       // Spaces/underscores → hyphens
    $text = preg_replace('/-+/', '-', $text);           // Collapse multiple hyphens
    $text = trim($text, '-');                            // Trim leading/trailing hyphens
    return $text;
}

// Test
$titles = [
    "Hello World!",
    "PHP 8.3: What's New?",
    "  Extra   Spaces  ",
    "CamelCase & snake_case",
    "10 Tips for Better Code!!!",
    "---Leading & Trailing---",
];

foreach ($titles as $title) {
    echo sprintf("  %-35s → %s\n", "\"$title\"", slugify($title));
}

🎯 Quick Quiz

Question 1: Why should you use === when checking the result of strpos()?

Question 2: What does sprintf("$%.2f", 9.5) output?

Question 3: What's the difference between heredoc and nowdoc?

Question 4: What does the regex /^\d{3}-\d{4}$/ match?

Question 5: When should you use mb_strlen() instead of strlen()?

Summary

🎉 Key Takeaways

  • Measuringstrlen() / mb_strlen() for length; substr_count() for occurrences
  • Searchingstrpos() (use === false!), stripos(), or PHP 8's str_contains(), str_starts_with(), str_ends_with()
  • Extractingsubstr($str, $start, $length); negative offsets count from end
  • Replacingstr_replace() / str_ireplace() for simple replacements; supports arrays for multiple replacements
  • Casestrtolower(), strtoupper(), ucfirst(), ucwords()
  • Trimmingtrim(), ltrim(), rtrim(); can trim custom characters
  • Splitting/Joiningexplode() splits string → array; implode() joins array → string; str_split() for chunks
  • Formattingsprintf() for precise formatting; number_format() for display numbers
  • Heredoc/Nowdoc — Multiline strings; heredoc interpolates, nowdoc is literal
  • Regexpreg_match() to test, preg_match_all() to find all, preg_replace() to replace, preg_split() to split

📚 Additional Resources

🚀 What's Next?

With arrays and strings mastered, you're ready to start building real web applications. In Lesson 11: Working with Forms, you'll learn how to receive user input through HTML forms, handle $_GET and $_POST data, process multi-value inputs, and create sticky forms.

🎉 Module 3 Complete!

You've finished the Data Structures module! You now have a solid command of PHP's two most important data types — arrays and strings. Time to put them to work with real web forms!