Single vs Double Quotes: What the Parser Rewrites
PHP has two literal syntaxes for the same string type, and the difference is entirely about what the parser rewrites before the string exists:
$name = 'Ada';
echo "Hello, $name!\n"; // "Hello, Ada!" + newline — variable substituted, \n is a newline
echo 'Hello, $name!\n'; // "Hello, $name!\n" literally — 14 chars, backslash included
Double-quoted strings interpolate variables and process escapes. The recognized escape sequences are the familiar set — \n, \t, \r, \\, \", \$ — plus \u{1F600} for Unicode code points (PHP 7+), which is the clean way to embed non-ASCII characters in source without hunting for the literal glyph. A $ followed by a valid variable name is substituted; writing a literal $ before a word requires \$.
Single-quoted strings are almost entirely literal. Only two sequences are recognized: \\ (a backslash) and \' (a quote). Everything else arrives byte-for-byte, which is why single quotes are the default for anything with backslashes or dollar signs — Windows paths, regex patterns, shell fragments — and for plain text with no variables in it:
$re = '/^[a-z]+$/i'; // no double-escaping \s \d \w headaches
$path = 'C:\php\php.exe'; // one backslash in, one backslash out
The performance question ("are double quotes slower?") has a modern answer: negligibly, and not for the reason people think. Interpolation is resolved at compile time from the literal parts — the engine builds the concatenation plan once — so a double-quoted string with one variable is comparable to the explicit concatenation 'Hello, ' . $name . '!'. The old advice to prefer single quotes for speed is stale; the real reason to prefer single quotes is intent: a string with no interpolation in it cannot accidentally interpolate, and readers can see at a glance that nothing in it is dynamic.
Interpolating an undefined variable inside double quotes behaves like reading it anywhere else — a PHP 8 warning and an empty substitution — so the complex syntax pairs naturally with the null-coalescing operator when the value may be absent:
echo "Hello, {$user['name'] ?? 'guest'}"; // no warning, sensible default, one expression
The same brace form keeps a literal $ legible in a template without escaping it ("{$dollar}9.99"), and \u{...} gives an ASCII-only source a way to embed a bullet or an accented name without depending on the file's encoding surviving every git operation:
echo "Caf\u{00E9} \u{2014} open 8\u{2013}18\n"; // Café — open 8–18
Those two features — brace-wrapped defaults and \u{} escapes — cover nearly every place where the simple "$var" form runs out of road.
One placement rule covers array access and object properties in the simple syntax: "$user[name]" and "$user->name" work only for unquoted keys and plain property names. The moment a key needs quotes or the expression gets any more complex, you must use the brace form of the next chapter — $user['name'] inside a double-quoted string is a parse error without them, and it is one of the two or three interpolation facts every PHP interview eventually probes.
Complex Syntax: When a Dollar Sign Needs Braces
The simple "$user->name" form covers property reads and bare-key array access. The complex syntax {$ ... } covers everything else by making the boundary explicit:
echo "Order {$order['id']} for {$customer->firstName}"; // quoted key, property
echo "Total: {$cart->total($tax)}"; // method call — allowed!
echo "Path: {$matrix[$i][$j]}"; // nested access
echo "Sum: {$a} {$b}"; // braces prevent ambiguity: "$a {$b}" vs "$ab"
Anything valid inside {$ ... } is substituted — including method calls, arithmetic, and function calls — because the braces hand a real expression to the parser rather than asking it to guess where a variable name ends. The ambiguity case is the one to remember in reviews: "$user_id" is one variable named user_id; when you mean $user followed by the literal text _id, only braces ("{$user}_id") say so.
One syntax to retire on sight: ${name} interpolation is deprecated since PHP 8.2 (scheduled for removal in 9.0) in favor of {$name}. Legacy codebases still contain it, and a deprecation notice in the log is exactly how you find those files.
Escaping is the other half of complex syntax. Inside double quotes and heredocs, \$ produces a literal dollar, and doubled backslashes collapse as usual. But the safest escape from interpolation entirely is single quotes or nowdoc — a string containing user-shaped text ("$100 off", SQL with $_-prefixed placeholders, JSON templates) reads wrong when every $ needs a backslash. A useful middle path is printf, which keeps the template and the values separate:
printf('Order %s for %s: %.2f EUR', $id, $name, $total);
No $ parsing happens in the format string at all, positional placeholders survive translation workflows, and numeric formatting (%.2f, %05d) comes for free — the same reason it appeared in the output chapter.
The rule of thumb that resolves style debates: interpolate when the string is prose with holes; concatenate or printf when the pieces are computed. "Total: {$total} EUR" reads better than 'Total: ' . $total . ' EUR', while building a query from ten fragments with concatenation makes the seams visible — and visible seams are what let the prepared-statement habit (next chapter's neighbor) take hold.
If you do mix interpolation with concatenation and arithmetic, one PHP 8 precedence change belongs in your mental model. Before 8.0, . bound tighter than + and -, so 'Total: ' . $a + $b parsed as ('Total: ' . $a) + $b — string-converting $a, then adding — and producing a nonsense number where a total was expected. PHP 8 reordered the precedence so arithmetic happens first:
'Total: ' . $a + $b // PHP 7: ('Total: ' . $a) + $b — wrong
// PHP 8: 'Total: ' . ($a + $b) — as written
Mixed expressions like this still deserve parentheses for the reader's sake, but the change explains a whole class of "the upgrade changed my output" bug reports — the same version-sensitivity theme as 0 == 'apples'.
The interpolation-vs-formatting choice in one comparison:
"Hello, {$name} — you have {$count} messages" // prose with holes: interpolate
sprintf('%s ordered %d units at %.2f', $who, $n, $p) // values with types: format
'prefix_' . $id // computed fragments: concatenate
Each form makes a different promise about where the dynamic parts are, and choosing deliberately is most of what "string style" means in a reviewed codebase.
Heredoc and Nowdoc: Multi-line Literals Without the Backslash Forest
When a literal spans several lines — SQL, HTML fragments, CLI help text, JSON payloads — escaping quotes and newlines in one long double-quoted string becomes unreadable. Heredoc solves it with a start marker and a closing identifier:
$sql = <<<SQL
SELECT id, email
FROM users
WHERE status = 'active'
AND created_at > :since
SQL;
The rules that matter:
<<<IDENTopens; the identifier alone on a line closes it (with an optional;).- Since PHP 7.3, the closing identifier may be indented, and that indentation is stripped from every line — so heredoc lives inside indented code without injecting leading spaces into the string.
- Content between the markers behaves exactly like a double-quoted string: variables interpolate and escapes like
\nare processed.
That last point is what separates heredoc from its twin. Nowdoc — with a quoted identifier — is the single-quoted equivalent: completely literal, no interpolation, no escapes:
$pattern = <<<'REGEX'
/^[a-z][a-z0-9_]{2,15}$/i
REGEX;
The pairing is easy to remember: heredoc ≈ double quotes (dynamic), nowdoc ≈ single quotes (static). Both support complex syntax — {$user->name} works inside heredoc — and both produce ordinary strings, so every function from the string topic accepts them.
Where they pay off in practice: any multi-line text with quotes in it. A JSON API payload no longer needs \" at every corner; an email template keeps its paragraphs; a CLI usage screen prints aligned columns:
$usage = <<<TXT
Usage: php import.php --file=<csv> [--dry-run]
--file=PATH CSV to import (required)
--dry-run Report what would happen; change nothing
TXT;
PHP 7.3 also removed the old requirement that a heredoc be assigned before use: it is a normal expression now, so a heredoc can sit inside a function call as an argument, which is where most modern code meets it:
$db->exec(<<<SQL
UPDATE orders SET status = 'packed' WHERE id = :id
SQL);
$resp = $client->post('/notify', [
'body' => json_encode(['text' => $body], JSON_PRETTY_PRINT),
]);
The indentation-stripping rule has one sharp edge worth internalizing: the closing marker's indentation is removed from every line, so a line inside the block that is indented less than the closing marker is a parse error, not a shorter string. Aligning the closing marker at the code's indentation level is what makes the feature safe to use inside classes and control structures.
And when the content must be truly inert, nowdoc is the vehicle of choice — regex patterns, serialized config, anything where a stray $ or backslash would silently change meaning:
$pattern = <<<'REGEX'
/^[a-z][a-z0-9_]{2,15}$/i
REGEX;
$banner = <<<'ASCII'
** LEGACY NOTICE **
This endpoint retires 2026-12-31.
ASCII;
The trap to state even though it belongs to a different topic: a heredoc-built SQL string is still string-built SQL. The syntax eliminated quote-escaping, not injection — placeholders plus prepared statements ($stmt->execute([':since' => $since])) remain mandatory, and heredoc is simply the pleasant way to write the statement template. Confusing "readable SQL" with "safe SQL" is a mistake interviewers actively look for, and the honest answer — heredoc for ergonomics, bindings for safety — is what they want to hear.
Quiz
1. What does `echo 'Cost: \$5\n';` print?
2. Which of these is legal interpolation in a double-quoted string?
3. Heredoc vs Nowdoc — the essential difference?
4. You build SQL in a heredoc with a variable interpolated into the WHERE clause. What is the correct security statement?
Flashcards
Question
What do double-quoted and single-quoted strings each process?
Click to reveal answer
Answer
Double-quoted: variable interpolation plus escapes \n \t \r \\ \" \$ \u{...}. Single-quoted: literal text with only \\ and \' recognized. Prefer single quotes for text with backslashes or no variables; performance differences are negligible today.
Question
When is the {$...} complex syntax required inside a double-quoted string?
Click to reveal answer
Answer
Quoted array keys ("{$row['id']}"), nested access, method calls ("{$cart->total()}"), and any spot where the variable name would otherwise run into following text ("{$user}_id"). Simple syntax covers only $var, $var[unquotedKey], $obj->property.
Question
Status of ${name} interpolation
Click to reveal answer
Answer
Deprecated since PHP 8.2 and scheduled for removal in 9.0; use {$name} instead. Finding ${...} in a codebase is a reliable sign of pre-7.4-era string code worth scanning for other legacy patterns.
Question
Heredoc mechanics: closing marker, indentation, and interpolation
Click to reveal answer
Answer
<<<ID opens, the identifier alone on a line closes it (optional ;). Since 7.3 the closing marker may be indented and that indentation is stripped from every content line. Content behaves like double quotes — variables and escapes apply; <<<'ID' (nowdoc) is the fully literal variant.
Question
What does heredoc NOT change about SQL safety?
Click to reveal answer
Answer
Nothing. It only removes quote-escaping from the statement template; interpolating variables into SQL is still injection. Real safety is placeholders plus prepared statements ($stmt->execute([...])) — heredoc is ergonomics, bindings are security.
Revision Notes
Key Takeaways
- 1. Double quotes interpolate and process escapes; single quotes are literal except \\ and \' — choose by intent, not by speed myths
- 2. Complex syntax {$...} handles quoted keys, nesting, and method calls; simple syntax stops at $var[key], $obj->prop
- 3. ${name} is deprecated since 8.2 — rewrite to {$name} when found
- 4. Heredoc ≈ dynamic multi-line (double-quote semantics), nowdoc ≈ literal multi-line (single-quote semantics); 7.3+ allows indented closing markers with indentation stripping
- 5. Heredoc fixes SQL ergonomics only — prepared statements with placeholders remain the actual injection defense
Interview Tips
- • "Difference between ' and \" strings?" — interpolation and escapes, with the exact escape lists; add that the performance argument for single quotes is stale
- • "How do you interpolate an array value with a quoted key?" — {$row['id']} complex syntax; mention ${name} deprecation to show version awareness
- • "Heredoc or nowdoc for this SQL?" — heredoc for the template with placeholders, never for user values; the answer must pivot to prepared statements
- • "A template needs a literal $ sign — what are the options?" — \\$ in double quotes/heredoc, single quotes or nowdoc, or printf with %s so no $ parsing occurs at all
Cheat Sheet
String Interpolation and Heredoc
Quote rules:
| Syntax | Interpolates | Escapes processed |
|---|---|---|
"..." |
yes — $var, {$expr} |
\n \t \\ \" \$ \u{...} |
'...' |
no | only \\ and \' |
<<<ID ... ID (heredoc) |
yes — like double quotes | like double quotes |
<<<'ID' ... ID (nowdoc) |
no — like single quotes | none |
Interpolation forms:
"$user->name" // property, simple syntax
"$row[total]" // array with UNQUOTED key only
"{$row['total']}" // complex syntax — quoted keys, method calls, nesting
"{$user}_id" // braces when the next char would extend the variable name
"{$cart->total($tax)}" // method call inside the string is legal
${name} is deprecated since 8.2 — rewrite as {$name}.
Heredoc / Nowdoc (PHP 7.3+ indented closing marker, indentation stripped):
$sql = <<<SQL
SELECT id FROM users WHERE status = :status
SQL; // interpolates — template, not safety
$re = <<<'REGEX'
/^[a-z][a-z0-9_]{2,15}$/i
REGEX; // fully literal
Readability only: heredoc SQL still needs prepared statements; heredoc JSON needs no \" at the corners.