Strings
Working with text in PHP: single and double quotes, interpolation, heredoc and nowdoc, common string functions, formatting with sprintf, and multibyte text with mb_ functions.
Single and double quotes
A string is a piece of text. In PHP you write one between single
quotes ('…') or double quotes ("…"), and
unlike in Python or JavaScript the two are not the
same:
-
In single quotes, almost everything is taken
literally. Only two escape sequences work:
\'for a quote and\\for a backslash. A$is just a dollar sign. -
In double quotes, PHP expands variables (
$customerbecomes its value) and understands the usual escape sequences:\nnew line,\ttab,\",\\,\$for a literal dollar sign, and\u{20AC}for a Unicode character by its hexadecimal code.
<?php
$customer = 'Amara';
$kwh = 318.4;
echo 'Hello $customer\n', "\n"; // single quotes: literal text
echo "Hello $customer\n"; // double quotes: variables expanded
echo "Usage: {$kwh} kWh\tthis month\n";
echo 'It\'s a backslash: \\', "\n";
echo "Tariff: \u{20AC}0.27 per kWh\n";
Hello $customer\n Hello Amara Usage: 318.4 kWh this month It's a backslash: \ Tariff: €0.27 per kWh
The first line shows the difference: in single quotes,
$customer and \n stay as typed, so the
line break comes from the separate "\n".
echo accepts several values separated by commas and
prints them one after another.
Which to choose? Many teams use single quotes for plain text and
double quotes only where they want interpolation or escapes. That
way a $ in a price list or a regular expression is
never expanded by accident. The speed difference people used to
argue about is negligible.
Joining strings with the dot
PHP joins strings with the
concatenation operator,
a dot. The shorthand .= appends to the string in a
variable.
<?php
$ticket = 'INC-31877';
$customers = 412;
// Concatenation with the dot operator
echo 'Outage ' . $ticket . ' affects ' . $customers . ' customers.' . "\n";
// The same text with interpolation
echo "Outage $ticket affects $customers customers.\n";
$message = 'Planned work';
$message .= ' on Monday'; // append to the same variable
echo $message, "\n";
Outage INC-31877 affects 412 customers. Outage INC-31877 affects 412 customers. Planned work on Monday
Variables inside strings
Putting values into a string is called string interpolation. PHP has had it for decades, long before JavaScript's template literals or Python's f-strings. Double-quoted strings support two forms:
-
Simple syntax:
$name,$meter[id](an array element, with the key unquoted) or$order->total(an object property). -
Braces:
{$meter['id']},{$order->customer->name}or{$readings[0]['kwh']}. Everything that starts with{$and ends with}is a variable expression, so nested arrays, quoted keys and method calls such as{$order->total()}work.
<?php
$meter = ['id' => 'MTR-100231', 'kwh' => 1542.7];
$codes = ['KE', 'NL'];
echo "Meter $meter[id] read $meter[kwh] kWh\n"; // simple syntax: no quotes on the key
echo "Meter {$meter['id']} read {$meter['kwh']} kWh\n"; // braces: any variable expression
echo "Region: $codes[0]-$codes[1]\n";
$unit = 'kWh';
echo "Total: {$meter['kwh']}{$unit}\n"; // braces end the name clearly
echo "Total: $meter[kwh]$unit\n";
echo "Total: 1542.7 $units\n";
Meter MTR-100231 read 1542.7 kWh Meter MTR-100231 read 1542.7 kWh Region: KE-NL Total: 1542.7kWh Total: 1542.7kWh Total: 1542.7
The braces also tell PHP where a variable name ends. In
"$kwhkWh", PHP would look for a variable called
$kwhkWh; "{$kwh}kWh" is unambiguous. The
last line of the example shows what happens with a typo:
$units does not exist, so PHP inserts an empty string
and writes a warning (Warning: Undefined variable $units) to the error output, which is why the line ends in a space.
Only variables are expanded. A function call such as
"Total: strtoupper($unit)" prints the text literally;
call the function outside the string, or use
sprintf().
Heredoc and nowdoc
Heredoc
For text over several lines, such as an email body or a block of
SQL, quotes get awkward. A
heredoc starts with
<<< and a name of your choice (an identifier
such as TEXT or SQL), continues on the
next line, and ends with the same name on a line of its own. It
behaves like a double-quoted string: variables and escape sequences
are expanded, but you do not need to escape double quotes.
<?php
$meter = 'KE-0042';
$kwh = 318.4;
$letter = <<<TEXT
Dear customer,
Your meter $meter recorded {$kwh} kWh this month.
Kestrel Energy
TEXT;
echo $letter, "\n";
$template = <<<'TEXT'
Use $meter and {$kwh} as placeholders.
Indented lines keep their extra spaces.
TEXT;
echo $template, "\n";
Dear customer,
Your meter KE-0042 recorded 318.4 kWh this month.
Kestrel Energy
Use $meter and {$kwh} as placeholders.
Indented lines keep their extra spaces.
The closing marker may be indented, and PHP removes that much indentation from every line of the text. That is why the letter prints flush left even though it is indented in the code. Lines that are indented further than the marker keep the extra spaces, as the second string shows. The text never includes the final line break before the marker.
Nowdoc
A nowdoc is the single-quoted version:
the name after <<< is in single quotes
(<<<'TEXT'), and nothing inside is expanded.
Use it for text that contains many dollar signs or backslashes, such
as a code sample, a shell command or a template with its own
placeholders. The second string in the example above is a nowdoc:
$meter and {$kwh} stay as typed.
Everyday string functions
PHP has dozens of string functions. They are plain functions that
take the string as an argument (strlen($s),
trim($s)), not methods on the string as in Python and
JavaScript (s.strip(), s.trim()). A
handful covers most daily work:
<?php
$line = ' MTR-100231;Deventer;1542.7 kWh ';
$clean = trim($line);
echo "[$clean]\n";
echo strlen($clean), "\n";
echo strtolower($clean), "\n";
[$meter, $city, $reading] = explode(';', $clean);
echo $city, "\n";
echo substr($meter, 4), ' ', substr($meter, -3), ' ', substr($meter, 0, 3), "\n";
echo str_replace('kWh', 'kilowatt-hours', $reading), "\n";
echo implode(' | ', [$meter, $city]), "\n";
echo str_pad('42', 6, '0', STR_PAD_LEFT), ' ', str_repeat('-', 10), "\n";
echo ucwords('kestrel energy portal'), "\n";
[MTR-100231;Deventer;1542.7 kWh] 30 mtr-100231;deventer;1542.7 kwh Deventer 100231 231 MTR 1542.7 kilowatt-hours MTR-100231 | Deventer 000042 ---------- Kestrel Energy Portal
| Function | What it does |
|---|---|
strlen($s) |
Length in bytes (see bytes and characters) |
trim(), ltrim(),
rtrim()
|
Remove spaces, tabs and line breaks at both ends, the left or the right |
strtolower(), strtoupper(),
ucfirst(), ucwords()
|
Change case (ASCII letters only) |
substr($s, $start, $length) |
Part of a string; a negative start counts from the end |
str_replace($search, $replace, $s) |
Replace every occurrence (case-sensitive;
str_ireplace() ignores case)
|
explode($separator, $s),
implode($glue, $array)
|
Split a string into an array, and join an array into a string |
str_pad(), str_repeat() |
Pad to a fixed width; repeat a string |
str_contains(), str_starts_with(),
str_ends_with()
|
Test for a substring (PHP 8.0 and later) |
strpos(), stripos() |
Position of the first match, or false |
The line
[$meter, $city, $reading] = explode(';', $clean);
unpacks the three parts straight into variables;
Arrays explains this destructuring. For
splitting on a pattern rather than a fixed separator, PHP has
preg_split() and the other
preg functions.
Searching: strpos and str_contains
str_contains(), str_starts_with() and
str_ends_with() answer yes-or-no questions and return
true or false.
strpos() returns the position of the first
match, counted in bytes from 0, or false when there is
none. var_dump() shows the type as well as the value,
which makes the difference visible:
<?php
$subject = 'Outage in Deventer, ticket INC-31877';
var_dump(str_contains($subject, 'Deventer'));
var_dump(str_starts_with($subject, 'Outage'));
var_dump(str_ends_with($subject, '31877'));
var_dump(strpos($subject, 'INC'));
var_dump(strpos($subject, 'Zwolle'));
var_dump(stripos($subject, 'OUTAGE'));
bool(true) bool(true) bool(true) int(27) bool(false) int(0)
That mix of return types (an integer or false) is a
classic trap. If the text you look for is at the very start,
strpos() returns 0, and
0 counts as false in an if:
<?php
$subject = 'Outage in Deventer';
// Wrong: strpos() returns 0 here, and 0 counts as false in an if
if (strpos($subject, 'Outage')) {
echo "found (loose check)\n";
} else {
echo "not found (loose check)\n";
}
// Right: compare with false strictly, or use str_contains()
if (strpos($subject, 'Outage') !== false) {
echo "found (strict check)\n";
}
if (str_contains($subject, 'Outage')) {
echo "found (str_contains)\n";
}
not found (loose check) found (strict check) found (str_contains)
Strings are values
Assigning a string to another variable copies it, and string
functions return a new string instead of changing their argument.
You can change a single byte through an index,
$copy[0] = 'K', but that affects only that variable.
Square brackets read one byte; a negative index counts from the end.
<?php
$code = 'ke-0042';
$copy = $code; // strings are copied by value
$copy[0] = 'K'; // changing one byte of the copy
echo $code, "\n";
echo $copy, "\n";
echo $code[3], ' ', $code[-1], "\n"; // read one byte; negative counts from the end
echo strtoupper($code), "\n"; // functions return a new string
echo $code, "\n";
ke-0042 Ke-0042 0 2 KE-0042 ke-0042
This is different from Python and JavaScript, where strings are immutable and assigning to an index fails. In PHP it works, but because it works on bytes it is only safe for plain ASCII text.
Formatting with sprintf
sprintf() builds a string from a
format string and a list of values. Each placeholder starts
with % and ends with a letter that says how to format
the next value: %s for a string, %d for a
whole number, %f for a decimal number. Between them you
can set a width, padding, alignment and the number of decimals.
printf() does the same but prints the result instead of
returning it.
<?php
$customer = 'Amara Okafor';
$kwh = 318.4;
$price = 0.27;
echo sprintf('%s used %d kWh', $customer, $kwh), "\n";
echo sprintf('Amount due: %.2f EUR', $kwh * $price), "\n";
echo sprintf('Meter %05d', 42), "\n";
echo sprintf('[%-10s][%10s]', 'left', 'right'), "\n";
echo sprintf("[%'.10s]", 'dots'), "\n";
echo sprintf('%1$s, %2$s. Again: %1$s', 'Deventer', 'Zwolle'), "\n";
echo sprintf('%x %b %e %%', 255, 5, 1542.7), "\n";
printf("%-12s %8.1f kWh\n", 'Deventer', 1542.66);
Amara Okafor used 318 kWh Amount due: 85.97 EUR Meter 00042 [left ][ right] [......dots] Deventer, Zwolle. Again: Deventer ff 101 1.542700e+3 % Deventer 1542.7 kWh
| Placeholder | Meaning |
|---|---|
%s, %d, %f |
String, integer, floating-point number |
%.2f |
Two decimals (rounded) |
%05d |
Width 5, padded with zeros |
%10s, %-10s |
Width 10, right-aligned or (with -)
left-aligned
|
%'.10s |
Width 10, padded with a custom character (here a dot) |
%1$s, %2$s |
Use argument 1 or 2, so a value can be repeated or reordered |
%x, %b, %e,
%%
|
Hexadecimal, binary, scientific notation, a literal percent sign |
Note the first line of output: %d turned 318.4 into
318. It drops the decimals rather than rounding, so use
%.0f when you want a rounded whole number. Placeholders
such as %1$s contain a dollar sign, so write format
strings in single quotes; in double quotes PHP
would try to expand $s.
number_format
For amounts and large numbers, number_format() rounds
to a number of decimals and adds a thousands separator. Its third
and fourth arguments set the decimal point and the thousands
separator, which makes Dutch or German notation easy:
<?php
$amount = 1234567.891;
echo number_format($amount), "\n";
echo number_format($amount, 2), "\n";
echo number_format($amount, 2, ',', '.'), "\n"; // Dutch style
echo number_format(0.125, 2), ' ', number_format(-0.4), "\n";
1,234,568 1,234,567.89 1.234.567,89 0.13 0
Comparing strings
=== compares two strings byte by byte and is
case-sensitive. strcmp() and
strcasecmp() (which ignores case) return 0 when the
strings are equal, a negative number when the first sorts earlier
and a positive number when it sorts later.
strnatcmp() uses "natural order", in which
item9 comes before item10.
<?php
var_dump('Deventer' === 'deventer');
var_dump(strcasecmp('Deventer', 'DEVENTER'));
var_dump(strcmp('apple', 'banana') < 0);
var_dump('10' == '1e1'); // both numeric: compared as numbers
var_dump('10' === '1e1'); // strict: different strings
var_dump('item10' < 'item9'); // byte by byte: '1' comes before '9'
var_dump(strnatcmp('item10', 'item9') > 0);
bool(false) int(0) bool(true) bool(true) bool(false) bool(true) bool(true)
Lines 4 and 5 are the reason to prefer ===. With
==, two strings that both look like numbers are
compared as numbers, so '10' == '1e1' is true
(1e1 is 10 in scientific notation). With === they are
simply two different strings.
Operators and Comparison
has the full rules.
Bytes and characters
A PHP string is a sequence of bytes, not of
characters. PHP does not know or record which
character encoding
the bytes are in. Almost all text today is
UTF-8, in
which the letters A to Z take one byte each but a letter such as
ë takes two, and an emoji four. The classic string
functions all count and cut bytes:
'Zoë' in UTF-8: three characters, four
bytes. Byte functions such as strlen() and
substr() see the bottom row.
The example below counts the characters in two ways that work on any
installation: preg_match_all('/./u', …), where the
u modifier makes the regular expression engine read
UTF-8, and iconv_strlen() from the iconv extension. It
also shows what goes wrong when a byte function cuts a character in
half: substr($name, 0, 3) keeps only the first byte of
ë, and the result is no longer valid UTF-8 (the test
preg_match('//u', $cut) fails on invalid UTF-8).
Browsers show such a broken byte as a replacement character or as
garbage.
<?php
$name = 'Zoë';
echo strlen($name), " bytes\n";
echo preg_match_all('/./u', $name), " characters (preg, /u)\n";
echo iconv_strlen($name, 'UTF-8'), " characters (iconv)\n";
echo bin2hex($name), "\n";
$cut = substr($name, 0, 3); // cuts ë in half
echo bin2hex($cut), "\n";
var_dump(preg_match('//u', $cut) === 1); // still valid UTF-8?
echo iconv_substr($name, 0, 3, 'UTF-8'), "\n";
echo strtoupper($name), "\n"; // only a-z change
4 bytes 3 characters (preg, /u) 3 characters (iconv) 5a6fc3ab 5a6fc3 bool(false) Zoë ZOë
The last line shows a second limit: strtoupper() only
changes the ASCII letters a to z, so ë stays lowercase.
The same goes for strtolower(),
ucfirst() and ucwords().
The mb_ functions
The mbstring extension (multibyte string) adds a
multibyte string function
for most byte functions: mb_strlen(),
mb_substr(), mb_strtoupper(),
mb_strtolower(), mb_str_split(),
mb_strpos() and more. They work on characters in a
given encoding, UTF-8 by default. mbstring is a
PHP extension
that most hosting providers enable, but a minimal server may lack
it; on Debian and Ubuntu it is the package
php8.x-mbstring, and php -m lists what is
loaded.
<?php
// Needs the mbstring extension (php8.2-mbstring on Debian)
$name = 'Zoë';
echo strlen($name), "\n"; // bytes
echo mb_strlen($name), "\n"; // characters
echo mb_substr($name, 0, 2), "\n";
echo mb_strtoupper($name), "\n";
echo mb_str_split($name)[2], "\n";
4 3 Zo ZOË ë