Strings
|
This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production. The reference material consulted while preparing these pages is listed in the section bibliography. |
This page is written against PHP 8.5, checked on 2026-10-05. A PHP string is a sequence of bytes with no
encoding attached; the functions on this page work on bytes, which is exactly right for ASCII and binary data and
wrong for human text in other scripts. Character-aware processing with mbstring and intl is covered in
Unicode, mbstring and intl, and regular expressions in
Regular Expressions.
String literals
PHP has four ways to write a string literal.
Single-quoted
Everything is literal except \' (a quote) and \\ (a backslash). No variables are expanded and no other escapes
are recognised, which makes single quotes the safe default for fixed text.
<?php
declare(strict_types=1);
$title = 'Kindred';
echo 'It\'s called $title\n', "\n"; // It's called $title\n -- literally
echo 'C:\\Books\\', "\n"; // C:\Books\
Source: Single quoted strings
Double-quoted
Double quotes recognise escape sequences and interpolate variables:
| Escape | Meaning |
|---|---|
|
newline, carriage return, tab, vertical tab, form feed, escape (ESC) |
|
backslash, dollar sign, double quote |
|
a byte in octal |
|
a byte in hexadecimal |
|
the UTF-8 encoding of a Unicode code point (7.0) |
<?php
declare(strict_types=1);
echo "Title:\tKindred\n"; // a real tab and newline
echo "Price: \$12\n"; // literal dollar sign
echo "Shelf \u{1F4DA}\n"; // Shelf 📚
var_dump("\x41\101" === 'AA'); // bool(true)
Source: Double quoted strings
Interpolation
In double-quoted strings and heredocs, PHP expands variables in two forms:
-
Simple syntax:
$name,$array[key](unquoted key, one level),$array[0],$object->property(one level). -
Complex (curly) syntax:
{$expression}, where the expression starts with$and can be any variable, array access with quoted keys, property chain or method call. Use it whenever the simple form is ambiguous.
The third form, ${name} and ${expr}, is deprecated since PHP 8.2 and is scheduled for removal in PHP 9; write
{$name} instead.
<?php
declare(strict_types=1);
$title = 'Kindred';
$book = ['title' => 'Dawn', 'author' => ['name' => 'Octavia E. Butler']];
$loan = new class { public string $member = 'M-17'; public function due(): string { return '2026-10-26'; } };
echo "Simple: $title, $book[title], $loan->member\n";
echo "Complex: {$book['author']['name']}, due {$loan->due()}\n";
echo "Plural: {$title}s\n"; // braces separate the name from the following text
// echo "${title}"; // Deprecated (8.2): Using ${var} in strings is deprecated
Simple: Kindred, Dawn, M-17
Complex: Octavia E. Butler, due 2026-10-26
Plural: Kindreds
Function calls, constants and static properties cannot be interpolated directly; assign them to a variable first or
use sprintf().
Source: String interpolation
Heredoc and nowdoc
A heredoc (<<<ID … ID) behaves like a double-quoted string over several lines, without needing to escape
quotes. A nowdoc (<<<'ID' … ID) behaves like a single-quoted string. Since PHP 7.3 the closing marker may be
indented, and that indentation is removed from every line (flexible heredoc); the closing marker no longer has to be
followed by a newline either.
<?php
declare(strict_types=1);
$title = 'Kindred';
$year = 1979;
function card(string $title, int $year): string
{
return <<<TXT
Title: {$title}
Year: {$year}
(first edition)
TXT; // closing marker indentation (8 spaces) is stripped
}
echo card($title, $year), "\n";
$template = <<<'SQL'
SELECT title FROM books WHERE year > :year -- no interpolation: $year stays literal
SQL;
echo $template, "\n";
Title: Kindred
Year: 1979
(first edition)
SELECT title FROM books WHERE year > :year -- no interpolation: $year stays literal
Mixing tabs and spaces in the indentation of a flexible heredoc is a parse error, as is a body line indented less than the closing marker.
String offsets
A single byte of a string is accessed with $s[$i]; negative offsets count from the end (7.1). Reading past the end
yields a warning and an empty string. Writing $s[$i] = 'x' replaces one byte; assigning an empty string throws an
Error. The old curly-brace syntax $s{0} was removed in PHP 8.0.
<?php
declare(strict_types=1);
$s = 'Solaris';
var_dump($s[0], $s[-1], $s[10] ?? 'none'); // "S" "s" "none"
$s[0] = 'P';
var_dump($s); // string(7) "Polaris"
try {
$s[0] = '';
} catch (Error $e) {
echo $e->getMessage(), "\n"; // Cannot assign an empty string to a string offset
}
Offsets are bytes: 'Łódź'[1] is half of a two-byte character. Use mb_substr() for characters.
The string API by family
The string extension (book.strings) has well over a hundred functions. The ones below are those used daily, grouped
by purpose.
Searching
<?php
declare(strict_types=1);
$title = 'The Left Hand of Darkness';
// PHP 8.0: clear boolean checks
var_dump(str_contains($title, 'Hand')); // bool(true)
var_dump(str_starts_with($title, 'The ')); // bool(true)
var_dump(str_ends_with($title, 'ness')); // bool(true)
// Positions: strpos (case-sensitive), stripos (case-insensitive), strrpos (last occurrence)
var_dump(strpos($title, 'T')); // int(0)
var_dump(stripos($title, 'darkness')); // int(17)
var_dump(strrpos('banana', 'a')); // int(5)
// The strpos pitfall: position 0 is falsy
if (strpos($title, 'The') == false) { // WRONG: strpos() returns 0, and 0 == false
echo "BUG: 'The' reported as missing\n"; // this line runs
}
if (strpos($title, 'The') !== false) { // RIGHT
echo "found\n";
}
var_dump(strstr('reader@library.example', '@')); // string(16) "@library.example"
var_dump(strstr('reader@library.example', '@', true)); // string(6) "reader"
var_dump(substr_count('banana', 'an')); // int(2)
strpos() returns int|false. Since a match at the very beginning returns 0, comparing with == or using the
result in a boolean context reports "not found" for a string that starts with the needle. Use !== false, or
better, str_contains() when you only need a yes/no answer. Note that str_contains($s, '') is always true.
Source: str_contains and strpos
Slicing
<?php
declare(strict_types=1);
$isbn = '978-0-00-000001-1';
var_dump(substr($isbn, 0, 3)); // string(3) "978" start, length
var_dump(substr($isbn, -1)); // string(1) "1" negative start: from the end
var_dump(substr($isbn, 4, -2)); // string(11) "0-00-000001" negative length: stop before the end
var_dump(substr($isbn, 100)); // string(0) "" (8.0: empty string, no longer false)
Source: substr
Replacing
<?php
declare(strict_types=1);
// str_replace: search/replace, arrays allowed, optional count by reference
$clean = str_replace(['-', ' '], '', '978-0 00-000001-1', $count);
var_dump($clean, $count); // "9780000000011" int(4)
// Arrays are applied in order, so later replacements see earlier results:
var_dump(str_replace(['a', 'b'], ['b', 'c'], 'ab')); // string(2) "cc"
// strtr with an array replaces the longest matches first and never re-scans replaced text:
var_dump(strtr('ab', ['a' => 'b', 'b' => 'c'])); // string(2) "bc"
var_dump(strtr('Hi all', ['Hi' => 'Hello', 'all' => 'everyone'])); // "Hello everyone"
var_dump(str_ireplace('KIN', 'Kin', 'KINdred')); // case-insensitive: "Kindred"
var_dump(substr_replace('Hello', 'J', 0, 1)); // "Jello"
strtr() with an array is the right tool for template placeholders, because a replacement value can never be
replaced again; chained str_replace() calls can.
Source: str_replace and strtr
Splitting and joining
<?php
declare(strict_types=1);
$csvLine = 'Kindred,Octavia E. Butler,,1979';
print_r(explode(',', $csvLine)); // empty fields are kept
print_r(explode(',', $csvLine, 2)); // limit: at most 2 pieces
print_r(explode(',', $csvLine, -1)); // negative limit: drop the last piece
echo implode(' | ', ['Solaris', 'Kindred']), "\n"; // Solaris | Kindred
print_r(str_split('ABCDEFGHIJ', 4)); // ["ABCD", "EFGH", "IJ"]
echo wordwrap('The quick brown fox jumps', 10, "\n", true), "\n";
For real CSV, with quoted fields and embedded commas, use str_getcsv() or fgetcsv() instead of explode() (see
Files, Streams and I/O). str_split() splits bytes; for
characters use mb_str_split().
Padding, trimming and repeating
<?php
declare(strict_types=1);
var_dump(str_pad('7', 3, '0', STR_PAD_LEFT)); // "007"
var_dump(str_pad('Title', 10, '.')); // "Title....."
var_dump(trim(" Kindred \n")); // "Kindred" strips " \n\r\t\v\0"
var_dump(rtrim('1.500', '0')); // "1.5"
var_dump(trim('[draft]', '[]')); // "draft" custom character list
var_dump(trim('a..z', 'a..z')); // ".." "a..z" is the RANGE a-z, not "a", ".", "z"
var_dump(str_repeat('=', 5)); // "====="
The second argument of the trim() family is a list of characters, not a substring, and .. inside it denotes a
range. PHP 8.4 added multibyte-aware mb_trim(), mb_ltrim() and mb_rtrim().
Case
strtolower(), strtoupper(), ucfirst(), lcfirst() and ucwords() change the case of ASCII letters only.
Since PHP 8.2 they are locale-insensitive (before, the result depended on setlocale()), so strtolower('ÉLAN') is
'Élan': the É is untouched. Use mb_strtolower() / mb_convert_case() for other letters.
<?php
declare(strict_types=1);
var_dump(ucwords('the left hand of darkness')); // "The Left Hand Of Darkness"
var_dump(ucwords('jean-luc picard', " -")); // "Jean-Luc Picard": custom delimiters
var_dump(strtolower('ÉLAN'), mb_strtolower('ÉLAN')); // "Élan" "élan"
Source: strtolower and ucwords
Comparison and similarity
strcmp() returns a negative number, 0 or a positive number (only the sign is meaningful: the magnitude is an
implementation detail); strcasecmp() ignores ASCII case; strncmp()/strncasecmp() compare a prefix;
strnatcmp()/strnatcasecmp() use natural order, in which img10 comes after img9. For locale-correct comparison
of human text, use Collator from intl.
The similarity functions help with fuzzy search and "did you mean?":
-
levenshtein($a, $b)— the minimum number of single-byte insertions, deletions and substitutions (costs can be weighted); -
similar_text($a, $b, $percent)— the number of matching characters, and the similarity as a percentage; -
soundex($s)andmetaphone($s)— phonetic keys for English names: words that sound alike get the same key.
<?php
declare(strict_types=1);
var_dump(strcmp('Dawn', 'Kindred') < 0, strcasecmp('KINDRED', 'kindred')); // bool(true) int(0)
$files = ['img12.png', 'img10.png', 'img9.png'];
usort($files, strnatcmp(...));
echo implode(' ', $files), "\n"; // img9.png img10.png img12.png
// "Did you mean?" for a mistyped title
$titles = ['Kindred', 'Solaris', 'Dawn', 'Neuromancer'];
$query = 'Solarsi';
$best = array_reduce($titles, fn (?string $carry, string $t): string =>
$carry === null || levenshtein($query, $t) < levenshtein($query, $carry) ? $t : $carry);
echo "Did you mean {$best}?\n"; // Did you mean Solaris?
var_dump(levenshtein('kitten', 'sitting')); // int(3)
similar_text('World', 'Word', $percent);
printf("%.2f%%\n", $percent); // 88.89%
var_dump(soundex('Lem'), soundex('Lamb')); // "L500" "L510"
var_dump(metaphone('Knight')); // "NFT"
All of these operate on bytes; levenshtein() on two UTF-8 strings counts byte edits. PHP 8.5 added
grapheme_levenshtein() to the intl extension for character-level distance.
Source: levenshtein, similar_text, soundex, metaphone and strnatcmp
Formatting: printf, sprintf and number_format
printf() writes a formatted string, sprintf() returns it, fprintf() writes it to a stream, and vprintf()/
vsprintf() take the arguments as an array. A conversion specification is
%[argnum$][flags][width][.precision]specifier:
| Part | Values |
|---|---|
specifier |
|
flags |
|
width |
minimum number of characters |
precision |
digits after the decimal point for floats, maximum length for strings |
|
use argument number N (swap order, reuse arguments); write |
<?php
declare(strict_types=1);
printf("[%5d] [%-5s] [%05.1f] [%x] [%b] [%'*8s] [%+d]\n", 42, 'ab', 3.14159, 255, 5, 'pad', 7);
// [ 42] [ab ] [003.1] [ff] [101] [*****pad] [+7]
echo sprintf('%2$s by %1$s', 'Octavia E. Butler', 'Kindred'), "\n"; // Kindred by Octavia E. Butler
echo vsprintf("%-10s %4d\n", ['Solaris', 1961]); // Solaris 1961
// number_format(number, decimals, decimal separator, thousands separator)
var_dump(number_format(1234567.891)); // "1,234,568"
var_dump(number_format(1234567.891, 2)); // "1,234,567.89"
var_dump(number_format(1234567.891, 2, ',', '.')); // "1.234.567,89"
var_dump(number_format(-1234.567, 2, '.', ' ')); // "-1 234.57"
var_dump(sprintf('%.2f', 1.005)); // "1.00" -- 1.005 is really 1.00499999...
number_format() and %f round binary floats, so 1.005 formats as 1.00. For money, use integer minor units or
BcMath\Number and format with NumberFormatter (intl), which also handles currency symbols and locales.
Source: sprintf and number_format
HTML-related functions
PHP has several functions for HTML. The important one is htmlspecialchars(), which converts &, <, >, " and
' to entities so that text can be safely placed in HTML. Since PHP 8.1 its default flags are
ENT_QUOTES | ENT_SUBSTITUTE | ENT_HTML401, so single quotes are escaped and invalid UTF-8 is replaced with U+FFFD
instead of returning an empty string; pass the flags and 'UTF-8' explicitly anyway, so the intent is visible:
<?php
declare(strict_types=1);
function e(string $text): string
{
return htmlspecialchars($text, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
}
$review = '<script>alert("x")</script> Tom\'s & Jerry\'s';
echo e($review), "\n";
// <script>alert("x")</script> Tom's & Jerry's
echo nl2br(e("Line one\nLine two")), "\n"; // inserts <br /> before newlines
echo strip_tags('<b>bold</b> and <i>italic</i>', '<i>'), "\n"; // bold and <i>italic</i>
echo html_entity_decode('été & hiver'), "\n"; // été & hiver
-
htmlentities()converts every character that has a named entity; with UTF-8 output it is unnecessary. -
strip_tags()is not a sanitiser: it can be bypassed and keeps attributes on allowed tags. Escape on output instead; for user-supplied HTML that must be kept, use an HTML sanitiser library. -
Escaping depends on the context (HTML body, attribute, URL, JavaScript). The full treatment, with the errata from older books, is in Forms, Validation and Output Escaping.
Source: htmlspecialchars, strip_tags and nl2br
Common pitfalls
-
strpos() == falsefor a match at position 0 — use!== falseorstr_contains(). -
Byte functions on UTF-8 —
strlen('Łódź')is 7,substr(),str_split(),strrev()anducfirst()can cut or ignore multibyte characters. -
trim()character lists — the second argument is a set, and..defines a range. -
Interpolating method calls with the simple syntax —
"$obj->method()"interpolates only the property lookup, not the call; use{$obj->method()}. -
str_replace()with arrays applies replacements sequentially; usestrtr()for one-pass substitution. -
Building SQL, shell commands or HTML by concatenation — use prepared statements,
escapeshellarg()or argument arrays, and context-aware escaping.
Where older books and tutorials are outdated
-
$str{0}offset syntax was removed in 8.0;${var}interpolation is deprecated since 8.2. -
money_format()(Beak, Kromann) was removed in 8.0; useNumberFormatter::formatCurrency(). -
strpos($h, $n) !== falseeverywhere — still correct, butstr_contains(),str_starts_with()andstr_ends_with()(8.0) express the intent. -
Locale-dependent case and float formatting —
strtolower()and friends ignore the locale since 8.2, and float to string conversion since 8.0. -
htmlspecialchars()defaults — books that call it without flags assume the pre-8.1 default, which did not escape single quotes. -
utf8_encode()/utf8_decode()were deprecated in 8.2; usemb_convert_encoding().
References
-
PHP Manual — Strings (syntax, interpolation, heredoc/nowdoc, string offsets)
-
str_contains, str_starts_with, str_ends_with, strpos, substr, str_replace, strtr, explode, implode, str_pad, trim
-
strcmp, strnatcmp, levenshtein, similar_text, soundex, metaphone
-
PHP Manual — PHP 8.2 deprecated features (dollar-brace interpolation, utf8_encode)