Free Handbook · Every example compiled & verified

Strings

Single vs double quotes, interpolation, heredoc, the string functions you use daily, sprintf and number_format, UTF-8 with mb_*, comparison and regex.

0 / 148 lessons🔥 0 day streak
ShareXLinkedIn

Module 03 · what you'll be able to do

  • Choose single quotes, double quotes, heredoc or nowdoc, and interpolate variables, array keys and object properties
  • Use strlen, str_contains, substr, strpos (with !== false), str_replace, explode, implode and trim correctly
  • Format numbers and text with sprintf, printf and number_format
  • Handle UTF-8 text with the mb_* functions instead of byte-based ones
  • Compare strings safely and match or replace text with preg_match and preg_replace
01

Single quotes, double quotes and interpolation

A PHP string is a sequence of bytes. You write one in single or double quotes, and the difference matters. Single quotes are literal: what you type is what you get, except \' and \\. Double quotes understand escape sequences like \n (newline) and \t (tab), and they interpolate variables — "Hi $name" puts the value of $name into the string.

phpmain.php
<?php
$name = "Asha";
$qty = 3;

echo 'Hi $name\n', "\n";        // single: no interpolation, no \n
echo "Hi $name, you have $qty items\n";
echo "Tab:\tafter tab | quote: \" | dollar: \$name\n";

// arrays and objects: use braces for anything beyond a plain variable
$user = ['name' => 'Ravi', 'langs' => ['php', 'sql']];
echo "User: {$user['name']}, first language: {$user['langs'][0]}\n";
echo "Simple syntax works for one level: $user[name]\n";

$order = new stdClass();
$order->id = 1042;
$order->total = 59.5;
echo "Order #{$order->id} costs {$order->total}\n";

$item = "apple";
echo "Two {$item}s\n";          // braces mark where the name ends
Outputcompiled & run with real PHP
Hi $name\n
Hi Asha, you have 3 items
Tab:	after tab | quote: " | dollar: $name
User: Ravi, first language: php
Simple syntax works for one level: Ravi
Order #1042 costs 59.5
Two apples
Your turn

Remove the braces in the last line: "Two $items". Read the warning — PHP now looks for a variable called $items.

Which quotes should you use?
Use single quotes for fixed text and array keys ($user['name']), and double quotes when you interpolate or need \n. The speed difference is a myth — PHP compiles both once. Braces {$...} are the safe default inside double quotes: they work for array keys, nested arrays, properties and method calls like {$order->total()}.
Error you will hit

Parse error: quoted array key inside a string without braces

php
<?php
$user = ['name' => 'Asha'];
echo "Hi $user['name']\n";
Parse error: syntax error, unexpected string content "", expecting "-" or identifier or variable or number in main.php on line 3
Why PHP said that

Inside double quotes the "simple" syntax allows only $user[name], with the key unquoted. A quoted key confuses the string parser, and the message is famously unhelpful. The rule is easy: anything more than a plain variable goes in braces.

The fix

Wrap the expression in braces.

php
<?php
$user = ['name' => 'Asha'];
echo "Hi {$user['name']}\n";
Error you will hit

Warning: Array to string conversion

php
<?php
$tags = ['php', 'web'];
echo "Tags: " . $tags . "\n";
Warning: Array to string conversion in main.php on line 3
Tags: Array
Why PHP said that

An array has no automatic string form. PHP warns and prints the literal word Array — so a page can show "Tags: Array" to real users. The same happens with "Tags: $tags".

The fix

Decide how the array should look as text: join it with implode(), or debug it with print_r() / var_dump().

php
<?php
$tags = ['php', 'web'];
echo "Tags: " . implode(", ", $tags) . "\n";
02

Heredoc and nowdoc for multi-line text

For multi-line text — an email body, an SQL query, an HTML snippet — PHP has heredoc: <<<IDENT on one line, the text, then IDENT;. It behaves like a double-quoted string (interpolation and escapes work) but you do not escape quotes. Nowdoc, <<<'IDENT' with quotes, behaves like a single-quoted string: nothing is interpolated. Since PHP 7.3 the closing marker can be indented, and that indentation is removed from every line.

phpmain.php
<?php
$name = "Asha";
$total = 59.5;

$email = <<<TEXT
    Hi $name,
    Your order total is {$total} rupees.
      (This line keeps 2 extra spaces.)
    "Quotes" need no escaping.
    TEXT;
echo $email, "\n---\n";

$template = <<<'RAW'
    Dear $name, nothing is replaced in a nowdoc.
    RAW;
echo $template, "\n";
Outputcompiled & run with real PHP
Hi Asha,
Your order total is 59.5 rupees.
  (This line keeps 2 extra spaces.)
"Quotes" need no escaping.
---
Dear $name, nothing is replaced in a nowdoc.
Your turn

Indent the closing TEXT; by two more spaces than the body lines and run it. Read the error below.

Error you will hit

Parse error: Invalid body indentation level

php
<?php
$text = <<<EOT
Hello
  EOT;
echo $text;
Parse error: Invalid body indentation level (expecting an indentation level of at least 2) in main.php on line 3
Why PHP said that

The closing marker is indented by 2 spaces, so PHP removes 2 spaces from every body line — but Hello has none to remove. Every body line must be indented at least as far as the closing marker.

The fix

Line the closing marker up with (or to the left of) the least-indented body line.

php
<?php
$text = <<<EOT
  Hello
  EOT;
echo $text;
03

Concatenation and building strings

The dot . joins two strings, converting numbers to text as needed, and .= appends to a variable. In PHP 8, + and - bind tighter than ., so "Total: " . $a + $b adds first. To build a string from many pieces, collect them in an array and implode() once.

phpmain.php
<?php
$first = "Ada";
$last = "Lovelace";
echo $first . " " . $last . "\n";

$price = 10;
echo "Total: " . $price + 5, "\n";      // PHP 8: + happens first
echo "Sum: " . ($price + 5) . "\n";     // brackets say it clearly

$csv = "";
foreach (['id', 'name', 'email'] as $col) {
    $csv .= $col . ",";
}
echo rtrim($csv, ","), "\n";            // trim the trailing comma

// usually cleaner: build an array, join once
$parts = [];
foreach ([3, 1, 2] as $n) {
    $parts[] = "#$n";
}
echo implode(" ", $parts), "\n";
echo str_repeat("=-", 5), "\n";
Outputcompiled & run with real PHP
Ada Lovelace
Total: 15
Sum: 15
id,name,email
#3 #1 #2
=-=-=-=-=-
Error you will hit

TypeError: Unsupported operand types: string + string

php
<?php
$name = "Asha";
echo "Hello, " + $name;
Fatal error: Uncaught TypeError: Unsupported operand types: string + string in main.php:3
Stack trace:
#0 {main}
  thrown in main.php on line 3
Why PHP said that

In JavaScript, Java and Python + joins strings. In PHP + is only arithmetic, and neither string is a number, so PHP 8 throws a TypeError.

The fix

Join strings with the dot operator, or interpolate.

php
<?php
$name = "Asha";
echo "Hello, " . $name;
04

The string functions you use every day

PHP has around a hundred string functions. These are the ones in almost every codebase. Positions start at 0, and a negative offset counts from the end.

phpmain.php
<?php
$s = "  Learn PHP the right way  ";
$t = trim($s);                         // also ltrim / rtrim

echo "[$t] length ", strlen($t), "\n";
var_dump(str_contains($t, "PHP"));     // PHP 8
var_dump(str_starts_with($t, "Learn"), str_ends_with($t, "way"));

echo substr($t, 6, 3), "\n";          // from index 6, 3 chars
echo substr($t, -3), "\n";            // last 3
echo strtoupper($t), " | ", strtolower("PHP"), "\n";
echo ucfirst("hello world"), " | ", ucwords("hello world"), "\n";

echo str_replace("right", "modern", $t), "\n";
echo str_replace(["a", "e"], "*", "banana tree", $count), " ($count replaced)\n";

$csv = "red,green,,blue";
$colors = explode(",", $csv);          // string -> array
echo count($colors), " parts: ", implode(" | ", $colors), "\n";
print_r(explode(",", $csv, 2));        // limit: at most 2 parts
Outputcompiled & run with real PHP
[Learn PHP the right way] length 23
bool(true)
bool(true)
bool(true)
PHP
way
LEARN PHP THE RIGHT WAY | php
Hello world | Hello World
Learn PHP the modern way
b*n*n* tr** (5 replaced)
4 parts: red | green |  | blue
Array
(
    [0] => red
    [1] => green,,blue
)
Your turn

Use explode and array_filter to drop the empty part from $csv, then print the remaining colours joined with ", ".

strpos and the "0 is false" trap

strpos($haystack, $needle) returns the position of the first match, or false when there is none. The trap: a match at the very start returns 0, and 0 is falsy. So if (strpos(...)) treats "found at position 0" exactly like "not found". Always compare with !== false — or, when you only need yes/no, use str_contains().

VisualizeWhy the check says "not found" for a match at position 0Step 1 / 4
<?php
$email = "[email protected]";
$pos = strpos($email, "admin");
if ($pos) {
echo "found";
} else {
echo "not found";
}
Line 2

The string starts with admin.

Variables now
$email"[email protected]"
All 4 steps as a table
StepLineWhat happenedVariables now
12The string starts with admin.$email = "[email protected]"
23strpos finds admin at index 0 and returns the int 0.$email = "[email protected]" $pos = 0
34if ($pos) converts 0 to bool: false. The condition fails even though the text was found.$pos = 0
47The else branch runs. The fix is if ($pos !== false), which is true for 0.
phpmain.php
<?php
$email = "[email protected]";

$pos = strpos($email, "admin");
echo $pos !== false ? "found at $pos\n" : "not found\n";

$at = strpos($email, "@");
echo "user: ", substr($email, 0, $at), ", domain: ", substr($email, $at + 1), "\n";

var_dump(strpos($email, "root"));      // false, not -1
var_dump(stripos($email, "ADMIN"));    // case-insensitive
var_dump(strrpos("a.b.c", "."));       // last occurrence
var_dump(str_contains($email, "admin"));
Outputcompiled & run with real PHP
found at 0
user: admin, domain: example.com
bool(false)
int(0)
int(3)
bool(true)
In real jobs
Many PHP functions return false on failure and a value that can be 0 or "" on success — strpos, array_search, file_get_contents. Code reviews look for === false / !== false on every one of them. The PHP manual flags these with a red "Warning: this function may return Boolean false, but may also return a non-Boolean value which evaluates to false."
05

Formatting with sprintf, printf and number_format

sprintf($format, ...$values) returns a formatted string; printf prints it directly. Each % placeholder takes the next value: %s string, %d integer, %f float, %% a literal percent sign. Between the % and the letter you can add padding, width and precision.

phpmain.php
<?php
$item = "Keyboard";
$qty = 3;
$price = 49.5;

printf("%s x %d = %.2f\n", $item, $qty, $qty * $price);
echo sprintf("%05d", 42), "\n";          // zero-pad to width 5
echo sprintf("[%8s]", "right"), "\n";    // right-align in 8
echo sprintf("[%-8s]", "left"), "\n";    // left-align in 8
echo sprintf("%'.10d", 7), "\n";          // custom pad character
echo sprintf("%+d %+d", 5, -5), "\n";    // always show the sign
echo sprintf("%b %o %x %X", 10, 10, 255, 255), "\n";
echo sprintf("%.1f%%", 12.345), "\n";     // %% is a literal %
echo sprintf('%2$s, %1$s', "Ada", "Lovelace"), "\n"; // argument order

echo number_format(1234567.891), "\n";          // 0 decimals, rounded
echo number_format(1234567.891, 2), "\n";
echo number_format(1234567.891, 2, ",", "."), "\n"; // European style
Outputcompiled & run with real PHP
Keyboard x 3 = 148.50
00042
[   right]
[left    ]
.........7
+5 -5
1010 12 ff FF
12.3%
Lovelace, Ada
1,234,568
1,234,567.89
1.234.567,89
Your turn

Print a three-row receipt where item names are left-aligned in 12 characters and prices right-aligned in 8 with two decimals.

Format for display, not for maths
number_format returns a string with commas in it. Doing arithmetic on "1,234.50" fails, so format only at the moment you print. For currency and locale-aware formats (lakh grouping, currency symbols) use NumberFormatter from the intl extension.
06

UTF-8 and the mb_* functions

PHP strings are bytes, and the classic functions — strlen, substr, strtoupper, strrev, str_split — count and cut bytes. In UTF-8, é is 2 bytes and each Devanagari or Japanese character is 3. For any text a human typed (names, addresses, messages), use the mb_ ("multibyte") versions, which count characters (Unicode code points).

phpmain.php
<?php
$word = "café";
echo strlen($word), " bytes, ", mb_strlen($word), " characters\n";

echo strtoupper($word), " vs ", mb_strtoupper($word), "\n";
echo ucfirst("élan"), " vs ", mb_ucfirst("élan"), "\n";   // PHP 8.4+

$city = "München";
echo mb_substr($city, 0, 4), "\n";           // first 4 characters
echo count(str_split($city)), " byte chunks vs ",
     count(mb_str_split($city)), " characters\n";
echo implode("", array_reverse(mb_str_split($city))), "\n"; // safe reverse

$hindi = "नमस्ते";
echo strlen($hindi), " bytes, ", mb_strlen($hindi), " code points, ",
     grapheme_strlen($hindi), " visible characters\n";
Outputcompiled & run with real PHP
5 bytes, 4 characters
CAFé vs CAFÉ
élan vs Élan
Münc
8 byte chunks vs 7 characters
nehcnüM
18 bytes, 6 code points, 3 visible characters

A code point is not always what a reader sees as one character: in Hindi, vowel signs and the virama combine with consonants. grapheme_strlen() (from the intl extension) counts those visible units.

In real jobs
A classic production bug: substr($name, 0, 20) cuts a name in the middle of a two-byte character, and the database or JSON encoder rejects the broken UTF-8. Another: a length check with strlen rejects a 10-character Hindi or Arabic name as "too long". Use mb_substr / mb_strlen for user text, and keep everything UTF-8 end to end (database, HTML <meta charset>, source files).
Error you will hit

Warning: Uninitialized string offset

php
<?php
$code = "PHP";
echo $code[3];
Warning: Uninitialized string offset 3 in main.php on line 3
Why PHP said that

$code[3] reads one byte at index 3, but a 3-byte string only has indices 0 to 2. PHP warns and returns an empty string. $s[$i] is byte access, so on UTF-8 text it can also return half a character.

The fix

Check the length first, or use $s[-1] for the last byte. For characters of UTF-8 text use mb_substr($s, $i, 1).

php
<?php
$code = "PHP";
echo $code[strlen($code) - 1];   // P
07

Comparing strings

Use === to test whether two strings are equal. == treats two numeric strings as numbers, so "1e3" == "1000" is true. For ordering, strcmp() returns a negative number, 0 or a positive number (in PHP 8.2+ exactly -1, 0 or 1), and <=> does the same. strcasecmp() ignores case, and strnatcmp() sorts numbers inside text the way a human would.

phpmain.php
<?php
var_dump("1e3" == "1000");          // numeric strings: compared as numbers
var_dump("1e3" === "1000");         // strict: different strings
var_dump("abc" == "ABC");           // case matters
var_dump(strcasecmp("HELLO", "hello") === 0);

echo strcmp("apple", "banana"), " ", "b" <=> "a", "\n";

$files = ["img12.png", "img10.png", "img2.png"];
sort($files);                        // byte by byte
echo implode(" ", $files), "\n";
usort($files, 'strnatcmp');          // "natural" order
echo implode(" ", $files), "\n";

// comparing secrets: constant-time, strict
$expected = "a1b2c3";
$given = "a1b2c3";
var_dump(hash_equals($expected, $given));
Outputcompiled & run with real PHP
bool(true)
bool(false)
bool(false)
bool(true)
-1 1
img10.png img12.png img2.png
img2.png img10.png img12.png
bool(true)
Quick check

What does var_dump(strpos("php rocks", "php") == false); print?

08

Regular expressions: preg_match and preg_replace

PHP's regex functions start with preg_ (Perl-compatible). A pattern is a string with delimiters, usually slashes, and optional flags after the closing one: '/^\d{3}$/', '/php/i' (case-insensitive), '/\w+/u' (UTF-8 aware). Write patterns in single quotes so PHP does not process the backslashes first.

FunctionReturnsUse
preg_match($p, $s, $m)1, 0, or false on a bad patternDoes it match? Captures go into $m.
preg_match_all($p, $s, $m)Number of matchesFind every match.
preg_replace($p, $r, $s)The new stringSearch and replace; $1 refers to a group.
preg_split($p, $s)An arrayexplode with a pattern.
preg_quote($s)Escaped stringPut user input inside a pattern safely.
phpmain.php
<?php
$text = "Invoice 7781 is due on 2026-03-15";

if (preg_match('/(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/', $text, $m)) {
    echo "Day {$m['day']}, month {$m['month']}, year {$m['year']}\n";
    echo "Whole match: {$m[0]}\n";
}

preg_match_all('/#(\w+)/', "Learn #php and #regex today", $tags);
echo implode(", ", $tags[1]), "\n";

echo preg_replace('/\s+/', ' ', "too    many     spaces"), "\n";
echo preg_replace('/(\w+)@(\w+)\.com/', '$1 at $2', "mail [email protected]"), "\n";
print_r(preg_split('/[\s,]+/', "red, green  blue,yellow"));

var_dump(preg_match('/^\w+$/', "café"), preg_match('/^\w+$/u', "café"));
Outputcompiled & run with real PHP
Day 15, month 03, year 2026
Whole match: 2026-03-15
php, regex
too many spaces
mail asha at example
Array
(
    [0] => red
    [1] => green
    [2] => blue
    [3] => yellow
)
int(0)
int(1)

Without the u flag, \w works byte by byte and the two bytes of é are not "word" characters. Add u to any pattern that runs on user text.

Your turn

Write a pattern that checks a 6-digit PIN code (/^\d{6}$/) and test it with "560001" and "5600".

Error you will hit

Warning: preg_match(): Compilation failed

php
<?php
$ok = preg_match('/[0-9+/', 'order 42');
var_dump($ok);
Warning: preg_match(): Compilation failed: missing terminating ] for character class at offset 5 in main.php on line 2
bool(false)
Why PHP said that

The character class [0-9+ is never closed, so the pattern itself is invalid. preg_match warns and returns false — not 0. Code that checks if (!preg_match(...)) cannot tell "no match" from "broken pattern".

The fix

Fix the pattern, and when a pattern is built dynamically compare the result with === false and read preg_last_error_msg().

php
<?php
$ok = preg_match('/[0-9]+/', 'order 42');
var_dump($ok); // int(1)
Reach for a function before a regex
For fixed text, str_contains, str_replace and explode are faster and easier to read. For validating emails and URLs use filter_var($email, FILTER_VALIDATE_EMAIL) instead of a home-made pattern. Keep regex for real patterns: dates, codes, tokens.
Interpolation
Replacing variables inside a double-quoted string or heredoc with their values: "Hi $name", "{$user['name']}".
Heredoc / nowdoc
Multi-line string syntax <<<ID (interpolates, like double quotes) and <<<'ID' (literal, like single quotes).
Concatenation
Joining strings with . (and .= to append). + never joins strings in PHP.
Byte vs character
PHP strings are bytes; UTF-8 characters take 1–4 bytes. strlen counts bytes, mb_strlen counts characters.
Grapheme
What a reader sees as one character, which can be several code points (a letter plus accent or vowel sign).
Format specifier
A % placeholder in sprintf/printf, such as %05.2f.
Delimiter
The character that wraps a regex pattern in PHP, usually /, followed by flags like i and u.

Frequently asked questions

What is the difference between single and double quotes in PHP?
Single-quoted strings are literal: variables and escape sequences like \n are not processed (only \' and \\ are). Double-quoted strings interpolate variables and understand escape sequences. Performance is the same, so choose by whether you need interpolation.
Why does strpos return 0 and fail my if check?
strpos returns the position of the match, and a match at the start of the string is position 0, which is falsy. Compare with !== false, or use str_contains() when you only need to know whether the text is present.
Should I use strlen or mb_strlen?
Use mb_strlen for text people write, because strlen counts bytes and UTF-8 characters such as é or Hindi letters take several bytes each. strlen is right when you really mean bytes, for example the size of binary data or a header limit.

Finish the PHP handbook, then get hired

Sit the exam for your certificate, run your resume through the ATS checker, and see the jobs that ask for exactly this.

Check my resume
Found this course useful? Share it.
ShareXLinkedIn

Comments

0

Join the conversation. Sign in to leave a comment — we'd love to hear your thoughts.