PHP Strings
A string in PHP is a sequence of characters used to store and manipulate text: names, HTML fragments, JSON payloads, file paths, SQL snippets, and more. Strings are one of PHP’s most-used types, and PHP ships with well over a hundred built-in functions for searching, slicing, formatting, and transforming them. This lesson covers everything from basic quoting rules to how strings are represented internally, the pitfalls that trip up even experienced developers, and the functions you’ll reach for daily.
Overview: How Strings Work in PHP
Internally, a PHP string is stored as a structure (a zend_string) that holds the raw bytes, a cached length, and a hash used for fast array-key lookups. Unlike some languages, PHP strings are byte sequences, not Unicode code-point sequences. A string containing multi-byte UTF-8 characters (like accented letters or emoji) is really just a run of bytes to core functions like strlen() or substr() — they have no idea that several bytes together form one visible character. This is why PHP ships a separate mbstring extension (mb_strlen(), mb_substr(), etc.) for text that must be handled character-by-character.
PHP strings are also mutable: you can access and overwrite individual bytes using array-style indexing, e.g. $str[0] = 'H';. Because PHP uses copy-on-write for performance, if two variables reference the same underlying string buffer, mutating one of them forces PHP to duplicate the buffer first so the other variable is unaffected. Short, frequently used string literals (like function names or common keys) are also interned — stored once in memory and reused — which is why comparing many identical literals is cheap.
How a string literal is parsed depends entirely on which quoting style you use, and that decision is made by the Zend engine’s lexer at compile time, before your script ever runs. This matters more than it looks: it controls whether variables inside the string get replaced with their values (interpolation) and which backslash sequences are treated as special escapes.
Syntax
PHP supports four ways to write string literals. Each has different rules for interpolation and escaping:
| Style | Interpolates variables? | Escape sequences | Example |
|---|---|---|---|
| Single-quoted | No | Only \\ and \' |
'Price: $5' → literal $5 |
| Double-quoted | Yes ($var and {$var}) |
\n, \t, \", \\, \u{...}, etc. |
"Price: $5" |
Heredoc (<<<LABEL) |
Yes | Same as double-quoted | Multi-line templated text |
Nowdoc (<<<'LABEL') |
No | None | Multi-line literal text |
Strings are joined with the concatenation operator . (and appended in place with .=) — PHP uses + exclusively for numeric addition, not concatenation. Two interpolation forms exist inside double-quoted strings and heredocs: simple syntax ("Hello $name") and complex/curly syntax ("Hello {$name}"), the latter required whenever you need to interpolate an expression such as a property, method call, or array index, e.g. "{$user->name}" or "{$items[0]}".
Examples
Example 1: Quoting styles and interpolation
<?php
$firstName = "Ada";
$lastName = 'Lovelace';
$fullName = $firstName . " " . $lastName;
echo "Full name: $fullName\n";
echo 'Full name: $fullName' . "\n";
echo "Uppercase: " . strtoupper($fullName) . "\n";
Output:
Full name: Ada Lovelace
Full name: $fullName
Uppercase: ADA LOVELACE
The double-quoted string interpolates $fullName into its value, while the single-quoted string prints $fullName literally — single quotes never look inside for variables. The concatenation operator . joins the two names with a space, and strtoupper() converts the whole string to uppercase.
Example 2: Built-in string functions
<?php
$sentence = " PHP is a popular scripting language for web development. ";
$trimmed = trim($sentence);
$wordCount = str_word_count($trimmed);
$hasPhp = str_contains($trimmed, "PHP");
$replaced = str_replace("popular", "powerful", $trimmed);
$words = explode(" ", $trimmed);
$rejoined = implode("-", array_slice($words, 0, 3));
echo "Trimmed: $trimmed\n";
echo "Word count: $wordCount\n";
echo "Contains 'PHP': " . ($hasPhp ? "yes" : "no") . "\n";
echo "Replaced: $replaced\n";
echo "First three words joined: $rejoined\n";
Output:
Trimmed: PHP is a popular scripting language for web development.
Word count: 9
Contains 'PHP': yes
Replaced: PHP is a powerful scripting language for web development.
First three words joined: PHP-is-a
trim() strips the leading and trailing whitespace, str_word_count() counts alphabetic words, str_contains() (PHP 8+) checks for a substring without the old strpos() !== false dance, str_replace() swaps one substring for another, and explode()/implode() split a string into an array and rejoin a slice of it with a new separator.
Example 3: A realistic templated message
<?php
function maskAccountNumber(string $accountNumber): string
{
$lastFour = substr($accountNumber, -4);
return str_repeat("*", strlen($accountNumber) - 4) . $lastFour;
}
$customer = "jane doe";
$accountNumber = "1234567890123456";
$balance = 2450.5;
$name = ucwords($customer);
$masked = maskAccountNumber($accountNumber);
$formattedBalance = number_format($balance, 2);
$email = <<<EOT
Dear {$name},
Thank you for banking with us. Here is your account summary:
Account Number: {$masked}
Current Balance: \${$formattedBalance}
Have a great day!
EOT;
echo $email;
Output:
Dear Jane Doe,
Thank you for banking with us. Here is your account summary:
Account Number: ************3456
Current Balance: $2,450.50
Have a great day!
This example combines several tools at once: ucwords() capitalizes each word of the customer’s name, substr() with a negative offset grabs the last four digits of the account number, str_repeat() builds the masking asterisks, and number_format() renders the balance with a thousands separator and two decimal places. The heredoc (<<<EOT) lets us write a multi-line template that interpolates variables just like a double-quoted string, and the escaped \$ prints a literal dollar sign right before the interpolated {$formattedBalance}.
How It Works Step by Step (Under the Hood)
When the Zend engine compiles a double-quoted string or heredoc containing variables, it doesn’t treat the literal as one opaque blob. The lexer splits it into a sequence of parts: literal text chunks and variable/expression lookups. At runtime, PHP evaluates each variable (calling __toString() if it’s an object), converts it to a string if needed, and concatenates every part into one freshly allocated buffer sized exactly for the final result. This is more efficient than repeatedly calling . because the total length is computed once up front.
For a single-quoted string or nowdoc, no such scanning happens at all — the bytes between the quotes/markers are copied almost verbatim (only \\ and, for single quotes, \' are unescaped), which is why they’re marginally cheaper to parse and why developers are sometimes told to “prefer single quotes when you don’t need interpolation.”
When you mutate a string by index, e.g. $str[0] = 'H';, PHP first checks the string’s internal reference count. If the buffer is shared with another variable (copy-on-write), PHP duplicates it before writing so the other variable’s value is untouched; if it’s not shared, PHP writes the byte in place. This is also why indexing past the end of a string, or using a negative index PHP doesn’t support in write mode, produces warnings or unexpected results — you’re working directly with byte offsets, not a safe “character” abstraction.
Common Mistakes
Mistake 1: Expecting single quotes to interpolate variables
<?php
$user = "Maria";
echo 'Welcome back, $user!';
Output:
Welcome back, $user!
This is wrong because single-quoted strings never interpolate variables — $user is printed as literal text, not replaced with "Maria". Switch to double quotes (or heredoc) whenever you need a variable’s value embedded in the string:
<?php
$user = "Maria";
echo "Welcome back, $user!";
Output:
Welcome back, Maria!
Mistake 2: Using strlen() on multi-byte text
<?php
$name = "Fran\u{00e7}ois";
if (strlen($name) === 8) {
echo "Name has 8 characters.\n";
} else {
echo "Unexpected length: " . strlen($name) . "\n";
}
Output:
Unexpected length: 9
“Fran\u{00e7}ois” has 8 visible characters, but strlen() counts bytes, and the accented letter is encoded as 2 bytes in UTF-8, so strlen() reports 9. This is a very common source of off-by-one bugs whenever user-supplied text contains accents, non-Latin scripts, or emoji. Use the mbstring extension’s mb_strlen() (and mb_substr(), mb_strtoupper(), etc.) whenever you need to count or slice by character rather than by byte:
<?php
$name = "Fran\u{00e7}ois";
if (mb_strlen($name) === 8) {
echo "Name has 8 characters.\n";
} else {
echo "Unexpected length: " . mb_strlen($name) . "\n";
}
Output:
Name has 8 characters.
Best Practices
- Use single quotes for plain literal text and double quotes only when you actually need interpolation or escape sequences — it signals intent to readers.
- Reach for
mb_*functions (mb_strlen,mb_substr,mb_strtoupper) whenever text may contain multi-byte UTF-8 characters, such as user names or free-form input. - Prefer
str_contains(),str_starts_with(), andstr_ends_with()(PHP 8+) over manualstrpos() !== falsechecks for clarity. - Use
sprintf()ornumber_format()for formatted output instead of chains of concatenation. - Use heredoc for large templated blocks of text instead of long
.concatenation chains — it’s far easier to read. - Always escape untrusted strings with
htmlspecialchars()before echoing them into HTML to prevent XSS. - Concatenate with
., never+—+performs numeric addition and can throw aTypeErroron non-numeric strings in PHP 8.
Practice Exercises
- Write a function
reverseWords(string $sentence): stringthat reverses the order of the words (not the letters) in a sentence. For example,"PHP is fun"should become"fun is PHP". Hint: combineexplode(),array_reverse(), andimplode(). - Write a function
isStrongPassword(string $password): boolthat returnstrueonly if the password is at least 8 characters long and contains at least one digit and one uppercase letter. Hint: usemb_strlen()together withpreg_match(). - Given a paragraph of text and a target word, write a script that counts how many times the word appears, ignoring case. For the text
"PHP is great. I love php scripting."and the word"php", the expected result is2. Hint:strtolower()combined withsubstr_count().
Summary
- PHP strings are byte sequences; single quotes, double quotes, heredoc, and nowdoc differ in whether they interpolate variables and process escape sequences.
- Concatenate with the
.operator; use+only for numeric addition. - PHP provides dozens of built-in functions —
strlen(),substr(),str_replace(),explode()/implode(),sprintf(), and more — that you should use instead of hand-rolled loops. strlen()andsubstr()operate on bytes, not characters; use themb_*functions for multi-byte-safe text handling.- Strings are mutable and indexable by byte position, and PHP uses copy-on-write to keep shared strings efficient in memory.
- Always escape untrusted strings before rendering them as HTML to avoid security issues like XSS.
