PHP · Intermediate
Strings in PHP
In short: PHP strings are sequences of bytes written in single quotes (taken literally), double quotes (escape sequences and $variable interpolation) or heredoc blocks. Functions such as strlen, substr, strpos, str_replace, str_contains and sprintf do most everyday work; for text outside ASCII, the mb_ functions count and slice characters instead of bytes.
Writing strings and pulling them apart
The quote style decides how much PHP does to the text. Inside single quotes only \' and \\ are special; everything else, including $name and \n, is kept as written. Inside double quotes \n, \t and \\ become newline, tab and backslash, and variables are expanded: "Hello $name". When the variable is followed by characters that could be part of a name, or when you need an array element or a property, wrap the expression in braces: "{$prices[$item]} each". A heredoc, <<<TXT ... TXT;, behaves like a long double-quoted string and keeps its line breaks; since PHP 7.3 the closing marker may be indented and that indentation is removed from every line. A nowdoc, <<<'TXT', is the single-quoted equivalent.
Strings are joined with . and extended with .=. They are immutable in the sense that every function returns a new string and leaves the original alone; strtoupper($s) does nothing to $s unless you assign the result back.
A string is a sequence of bytes. strlen counts bytes, substr($s, $start, $length) slices by byte offset (negative values count from the end), and strtoupper changes only ASCII letters. For ASCII text these are exactly what you want and they are fast. For text with accents, other alphabets or emoji, use the mb_ family from the mbstring extension, enabled on this site: mb_strlen, mb_substr, mb_strtoupper and, from PHP 8.4, mb_ucfirst and mb_trim. They count characters rather than bytes.
Searching: str_contains, str_starts_with and str_ends_with (PHP 8.0) return booleans and read clearly. strpos($haystack, $needle) returns the byte offset of the first match or false, and because the offset can be 0 you must test it with !== false. str_replace($search, $replace, $subject) replaces every occurrence and accepts arrays for bulk replacement. trim, ltrim and rtrim remove whitespace, or a set of characters you pass, from the ends. explode($separator, $s) splits into an array and implode($glue, $array) joins one. ucfirst, ucwords, strtolower and strtoupper change case; strrev reverses bytes; str_repeat and str_pad build padding.
Formatting is sprintf's job: %s for a string, %d for an integer, %.2f for a float with two decimals, %5d for right alignment in five columns and %-10s for left alignment in ten. printf prints instead of returning, and number_format($n, 2) adds thousands separators. Comparing strings with === is exact; strcmp returns negative, zero or positive for ordering and strnatcmp orders img2 before img10.
Syntax
$a = 'literal $text\n'; // no interpolation, no escapes except \' and \\
$b = "expanded $name\n"; // interpolation and escapes
$c = "item: {$prices[$item]}"; // braces for expressions
$d = <<<TXT
Multi-line text with $name
keeps its line breaks.
TXT; // closing indentation is stripped (PHP 7.3)
$s = $a . $b; $s .= "more";
strlen($s); mb_strlen($s);
substr($s, 0, 5); substr($s, -3); mb_substr($s, 0, 5);
str_contains($s, "x"); str_starts_with($s, "x"); strpos($s, "x"); // int or false
str_replace("-", "/", $s); trim($s); strtoupper($s); ucfirst($s); ucwords($s);
explode(",", $s); implode(", ", $parts); str_repeat("-", 20); str_pad($s, 10, " ", STR_PAD_LEFT);
sprintf("%-10s %5d %8.2f", $name, $qty, $price); number_format($n, 2);strpos returns 0 for a match at the start; test with !== false. strlen and substr work in bytes, mb_strlen and mb_substr in characters.
Quotes, interpolation and heredoc
The same coffee order printed with single quotes, double quotes, braces and a heredoc.
<?php
$item = "oat latte";
$size = "large";
$prices = ["oat latte" => 4.5];
echo 'Single quotes keep $item as is\n', "\n";
echo "Double quotes expand: $size $item\n";
echo "Braces for expressions: {$prices[$item]} each\n";
echo 'Concatenation ' . 'works ' . 'in ' . 'both' . "\n";
$order = <<<TXT
Order summary
$size $item
Total: {$prices[$item]}
TXT;
echo $order, "\n";Output
Single quotes keep $item as is\n Double quotes expand: large oat latte Braces for expressions: 4.5 each Concatenation works in both Order summary large oat latte Total: 4.5
The first line prints $item and the two characters \n literally, because single quotes expand nothing; the line break comes from the separate "\n". The second line expands both variables. The third needs braces because $prices[$item] is an array lookup with a variable key. In the heredoc, the four spaces before the closing TXT are removed from every line, so the summary lines keep only their extra two spaces of indentation.
Searching, slicing and replacing
A warehouse code is inspected with the most common string functions.
<?php
$code = "SHELF-B12-OAK";
echo strlen($code), "\n";
echo strtolower($code), "\n";
echo substr($code, 0, 5), "\n";
echo substr($code, -3), "\n";
var_dump(strpos($code, "B12"));
var_dump(strpos($code, "PINE"));
echo str_contains($code, "OAK") ? "oak shelf" : "other", "\n";
echo str_replace("-", "/", $code), "\n";
[$kind, $slot, $wood] = explode("-", $code);
echo ucfirst(strtolower($wood)), " in slot ", $slot, "\n";
echo str_starts_with($code, "SHELF") ? "shelf" : "not shelf", "\n";Output
13 shelf-b12-oak SHELF OAK int(6) bool(false) oak shelf SHELF/B12/OAK Oak in slot B12 shelf
substr with a negative start counts back from the end, so -3 gives the last three characters. strpos finds B12 at byte 6 and returns false, not -1, for a missing needle. explode splits on the hyphen and the array is unpacked straight into three variables. ucfirst(strtolower(...)) is the usual way to normalise a word's case.
Formatting a receipt and counting characters
printf lines up columns for a bakery receipt, then strlen and mb_strlen disagree about an accented name.
<?php
$lines = [
["Sourdough loaf", 2, 3.9],
["Cinnamon knot", 3, 2.25],
["Cafe au lait", 1, 3],
];
$total = 0;
foreach ($lines as [$name, $qty, $price]) {
$sub = $qty * $price;
$total += $sub;
printf("%-16s x%d %8.2f\n", $name, $qty, $sub);
}
echo str_repeat("-", 28), "\n";
printf("%-16s %8.2f\n", "Total", $total);
$dessert = "Crème brûlée";
echo strlen($dessert), " bytes, ", mb_strlen($dessert), " characters\n";
echo mb_strtoupper($dessert), " | ", strtoupper($dessert), "\n";Output
Sourdough loaf x2 7.80 Cinnamon knot x3 6.75 Cafe au lait x1 3.00 ---------------------------- Total 17.55 15 bytes, 12 characters CRÈME BRÛLÉE | CRèME BRûLéE
%-16s pads the name to 16 columns on the right, %d prints the quantity, and %8.2f right-aligns the subtotal in 8 columns with two decimals, so the numbers line up. The three accented letters each take two bytes in UTF-8, which is why strlen says 15 while mb_strlen says 12. strtoupper leaves those bytes untouched, producing the odd mixed case; mb_strtoupper understands the characters.
Common mistakes
Testing strpos with ! or == false
Why it goes wrong: A match at the very start returns 0, and
0 == falseis true, soif (!strpos($s, "a"))treats a found needle as missing.Fix: Compare with
!== false, or usestr_containswhen you only need yes or no.PHP · fix<?php $s = "apple"; if (strpos($s, "a") !== false) { echo "found\n"; } if (str_contains($s, "a")) { echo "found again\n"; }Expecting single quotes to expand variables
Why it goes wrong:
'Hello $name'prints the seven characters$nameliterally; only double quotes and heredocs interpolate.Fix: Use double quotes when the text contains variables, or concatenate with
..Counting or slicing UTF-8 text with strlen and substr
Why it goes wrong: They work in bytes.
substr("Crème", 0, 3)cuts through the two-byteèand returns broken text, andstrlenoverstates the length.Fix: Use
mb_strlenandmb_substrfor any text that may contain non-ASCII characters.Joining with + instead of .
Why it goes wrong:
"Room " + 12throwsTypeErrorin PHP 8; with two numeric strings,"1" + "2"quietly gives 3 instead of"12".Fix: The dot operator is the only way to concatenate:
"Room " . 12.
Ways to write a string
| Form | Interpolation | Escapes | Use for |
|---|---|---|---|
| 'text' | no | \' and \\ only | fixed text, regular expressions |
| "text" | yes | \n \t \\ \$ \" and unicode \u{...} | messages with variables |
| <<<TXT ... TXT; | yes | as double quotes | multi-line templates |
| <<<'TXT' ... TXT; | no | none | multi-line literal text |
Where you use this
Two jobs come up constantly: parsing a line of input into fields, and producing aligned output. A line such as name=Ada;role=admin is broken apart with explode(";", $line) and then each piece with explode("=", $pair, 2), trimming as you go. Going the other way, a report with columns that line up is a printf format string with widths, and a table separator is str_repeat. Both directions reward knowing the byte-versus-character distinction as soon as names with accents appear.
$fields = [];
foreach (explode(";", trim($line)) as $pair) {
[$key, $value] = explode("=", $pair, 2);
$fields[trim($key)] = trim($value);
}
printf("%-10s %s\n", "name", $fields["name"]);Key points
- Single quotes are literal; double quotes and heredocs expand
$variablesand escape sequences. - Use
{$expr}braces for array elements and properties inside double quotes. .concatenates; string functions return new strings and never modify their argument.strposreturns an offset orfalse; check with!== false, or usestr_contains.strlenandsubstrcount bytes;mb_strlenandmb_substrcount characters.explodesplits,implodejoins,str_replacesubstitutes,trimcleans the ends.sprintfandprintfalign columns and fix decimals:%-10s,%5d,%.2f.
Try it yourself
The program reads a product code such as shelf-b12-oak and prints it unchanged. Make it print the code in upper case with every hyphen replaced by a slash.
<?php
$code = trim(fgets(STDIN));
echo $code, "\n";
shelf-b12-oakSHELF/B12/OAK<?php
$code = trim(fgets(STDIN));
echo strtoupper(str_replace("-", "/", $code)), "\n";
Practise this
- Headline to URL slugMedium
Turn a blog headline into a URL slug in PHP: lowercase, replace runs of non-alphanumerics with one hyphen, trim, and handle empty results.
StringsNot started
- Initials from a full nameEasy
Read a full name and print its initials in uppercase. Practise explode, string indexing and strtoupper in PHP.
StringsNot started
Related lessons
- Variables and Types in PHPHow PHP variables are named and assigned, the scalar types int, float, string, bool and null, how input text becomes numbers, and where type juggling bites.8 min
- Arrays in PHPHow PHP arrays hold ordered lists: literals, indexing, appending, count, foreach, searching, slicing and the array_map, array_filter and array_sum helpers.9 min
- JSON and Data Handling in PHPEncode PHP arrays to JSON and decode JSON into arrays, choose the right flags, avoid the list-versus-object trap, and turn raw input lines into structured.11 min
Frequently asked questions
What is the difference between single and double quotes in PHP?
Single-quoted strings are taken literally: $name stays as those five characters and \n is a backslash followed by n. Double-quoted strings expand variables, support {$expression} and interpret escape sequences such as \n and \t. Neither is faster in any way you will notice; choose by whether you need interpolation.
How do I get the length of a UTF-8 string in PHP?
Use mb_strlen($s), which counts characters. strlen($s) counts bytes, and in UTF-8 an accented letter takes two bytes, most other scripts three, and emoji four. The same distinction applies to substr versus mb_substr and strtoupper versus mb_strtoupper.
Why does strpos return 0 and break my if statement?
0 means the needle was found at the first byte. Because 0 is false in a condition, if (strpos(...)) wrongly treats that as not found. Always write strpos(...) !== false, or use str_contains, str_starts_with and str_ends_with, which return proper booleans.
How this page was checked. Every program on it was run with PHP 8.4 at build time by the publishing checks, and the output shown is what it printed. Running PHP inside the browser is not available yet, so the Run button is absent rather than pretending; copy the code and run it with PHP 8.4 locally.