Strings
A string stores text as a sequence of bytes. PHP gives you concise syntax for creating, joining, searching, cleaning, formatting, and outputting strings, but text handling becomes unsafe when code confuses raw data with a particular output context.
A useful workflow is: validate the input shape, normalise only what the application policy allows, preserve the intended text, then encode or escape it for the destination where it will be used.
Choose Quotes Deliberately
Single-quoted strings keep most content literal:
<?php
$name = 'Ada';
echo 'Hello, $name\n';
This prints the dollar sign, variable name, backslash, and n. In a single-quoted string, only \\ and \' have special escape behavior.
Double-quoted strings expand variables and recognised escape sequences:
<?php
$name = 'Ada';
echo "Hello, $name\n";
Output:
Hello, Ada
Use single quotes for literal text and double quotes when interpolation or escapes such as \n improve clarity. Consistency matters more than trying to avoid every concatenation operator.
Interpolate Simple Values Clearly
Braces make variable boundaries explicit:
<?php
$product = 'workbook';
$quantity = 2;
echo "{$quantity}x {$product}\n";
They are especially useful for array values:
<?php
$order = ['id' => 1042];
echo "Order {$order['id']}\n";
Use {$variable}, not the deprecated ${variable} interpolation form. For a complex calculation or function call, calculate first or concatenate the result rather than forcing too much logic into one string.
Concatenate With The Dot Operator
PHP joins strings with .:
<?php
$firstName = 'Ada';
$lastName = 'Lovelace';
$fullName = $firstName . ' ' . $lastName;
echo $fullName, PHP_EOL;
The compound operator .= appends to an existing string:
<?php
$message = 'Order 1042';
$message .= ': ready to ship';
Do not confuse . with numeric addition. Give formatted text and numeric calculations separate variables so units and types remain visible.
echo can accept several comma-separated arguments. That can avoid building a temporary string when you only need output:
echo 'Total: ', $totalCents, " cents\n";
Clean Boundaries Without Destroying Meaning
trim() removes whitespace from both ends, while ltrim() and rtrim() remove it from one side:
<?php
$rawName = " Ada Lovelace \n";
$name = trim($rawName);
var_dump($name);
Output:
string(12) "Ada Lovelace"
Trimming is appropriate for many form fields and file lines, but not every string. Leading spaces may matter in preformatted text, and trailing spaces may be part of a password. Normalisation must follow the field's contract.
Use str_replace() for literal replacements:
<?php
$reference = str_replace(' ', '-', 'ORDER 1042');
echo $reference, PHP_EOL;
It replaces every exact occurrence. Regular expressions are unnecessary for a fixed literal replacement and are covered separately later.
Search Without A Falsey-Position Bug
str_contains() answers a yes-or-no question:
<?php
$email = 'ada@example.com';
var_dump(str_contains($email, '@'));
Use str_starts_with() or str_ends_with() when the location matters at a boundary.
strpos() returns the byte offset of the first match or false when no match exists:
<?php
$reference = 'ORDER-1042';
$position = strpos($reference, 'ORDER');
var_dump($position);
The result is integer 0, which is falsey. This check is wrong:
if (strpos($reference, 'ORDER')) {
// Does not run for a match at position 0.
}
Compare with !== false, or use str_contains() when you do not need the offset.
Extract And Measure Carefully
substr() extracts by byte offset and length:
<?php
$reference = 'ORDER-1042';
$orderNumber = substr($reference, 6);
echo $orderNumber, PHP_EOL;
strlen() also counts bytes:
<?php
var_dump(strlen('PHP'));
var_dump(strlen('cafe'));
For plain ASCII text, bytes and visible characters align. UTF-8 characters may use multiple bytes. If an application needs human-character length or Unicode-aware slicing, use the Multibyte String extension functions such as mb_strlen() and mb_substr() with a known encoding, commonly UTF-8.
Do not enforce a user-visible character limit with strlen() unless the byte limit is intentional, such as a storage or protocol constraint.
Change Case With Encoding In Mind
strtolower() and strtoupper() are useful for ASCII identifiers and controlled tokens:
<?php
$status = strtoupper(trim(' paid '));
echo $status, PHP_EOL;
Human-language text may require mb_strtolower() or mb_strtoupper() with the Multibyte String extension. Case conversion can be language-sensitive, so do not assume a byte-oriented helper correctly normalises every person's name.
For machine identifiers, decide whether case is meaningful instead of lowercasing input automatically.
Format Numbers Before Joining Them To Text
Keep amounts as integers in cents during calculation, then format at output:
<?php
function formatCents(int $cents): string
{
return 'GBP ' . number_format($cents / 100, 2, '.', ',');
}
echo formatCents(123456), PHP_EOL;
Output:
GBP 1,234.56
The function makes separators and decimal places explicit. Formatting is presentation; do not parse the formatted string back into business calculations.
printf() can be useful for fixed-width CLI reports:
printf("%-12s %8s\n", 'Workbook', 'GBP 24.99');
Use it where alignment is part of the output contract, not merely to make a simple interpolation harder to read.
Escape For The Destination Context
Escaping is not one universal cleaning operation. The correct transformation depends on where the string will be inserted.
For HTML text content, use htmlspecialchars() at output time:
<?php
$name = '<script>alert(1)</script>';
$safeName = htmlspecialchars($name, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
echo "<p>Hello, $safeName</p>";
The browser receives text entities rather than executable markup. ENT_QUOTES handles both quote types, ENT_SUBSTITUTE replaces invalid encoding sequences, and the encoding is explicit.
Do not HTML-escape before storing a name in a database. Store the validated original text and escape when rendering into HTML. Early escaping can create double-encoding and makes the stored value unsuitable for email, JSON, or plain text.
Other destinations require other tools:
- URL path segments use
rawurlencode(); - query strings can be built with
http_build_query(); - JSON should be produced with
json_encode()and checked for failure; - SQL values belong in prepared-statement parameters, not manually quoted strings;
- shell arguments require APIs designed for process execution and careful command boundaries.
HTML escaping does not make text safe for SQL, JavaScript source, a shell command, or a URL.
Build Multiline Text Readably
Heredoc behaves like a double-quoted string and supports interpolation:
<?php
$name = 'Ada';
$message = <<<TEXT
Hello, $name
Your order is ready.
TEXT;
echo $message, PHP_EOL;
Nowdoc uses a single-quoted identifier and does not interpolate variables. Both are useful for readable multiline literals, but the closing identifier and indentation must follow PHP's syntax rules.
For HTML templates, prefer normal template structure over building large pages through concatenation. Escape dynamic values at each output location.
Distinguish Empty From Missing And Zero
PHP's empty() treats several different values as empty, including '', '0', 0, false, null, and an empty array. That is often broader than a text-field rule.
For required text, trim and compare explicitly:
<?php
$input = ' 0 ';
$value = trim($input);
if ($value === '') {
echo "Value is required\n";
} else {
echo "Accepted: $value\n";
}
The string '0' is present and remains valid. A loose truthiness check or empty($value) would reject it. Define whether whitespace-only, zero-like, and null values mean the same thing before choosing a helper.
Compare Strings According To The Rule
Use === when two strings must contain exactly the same bytes:
var_dump('paid' === 'paid');
var_dump('PAID' === 'paid');
Use strcmp() when a sorting callback needs a negative, zero, or positive comparison result. Do not depend on its exact non-zero number; only the sign is meaningful.
Case-insensitive comparisons can hide policy. Status identifiers should usually be normalised once or compared exactly. Human-language alphabetical ordering is a different problem and may require the Internationalization extension's Collator, not strtolower() plus sort().
Avoid Inefficient Or Unclear Repeated Assembly
Appending a few lines with .= is clear. When joining an existing list, implode() states the intent directly:
<?php
$errors = ['Name is required', 'Email is invalid'];
$message = implode('; ', $errors);
echo $message, PHP_EOL;
explode() performs the reverse split using a literal delimiter:
$parts = explode(',', 'paid,pending,failed');
Do not parse CSV files with explode(','); quoted fields, embedded commas, and line endings require CSV-aware functions such as fgetcsv() or str_getcsv().
For large output, avoid repeatedly copying one enormous string when it can be streamed or written in chunks. Start with clear code, then measure before optimising.
Common String Mistakes
| Symptom | Likely cause |
|---|---|
$name appears literally |
Used a single-quoted string expecting interpolation |
\n appears literally |
Used single quotes for an escape sequence |
| Prefix match is missed | Treated strpos() result 0 as false |
| Character limit rejects valid text | Used byte-counting strlen() for UTF-8 characters |
| Stored text contains entities | Escaped for HTML before persistence |
| HTML injection is possible | Printed untrusted text without HTML-context escaping |
| HTML escaping is used for SQL | Confused output encoding with database parameterisation |
| Text is unexpectedly changed | Applied trimming or case conversion without a field policy |
| Money calculation uses formatted text | Mixed presentation strings with numeric amounts |
What You Should Be Able To Do
After this lesson, you should be able to choose single or double quotes deliberately, interpolate variables with braces, concatenate and append strings, trim only at appropriate boundaries, replace and search literal text, compare strpos() with false correctly, distinguish byte length from character length, format integer-cent amounts, escape dynamic HTML with an explicit encoding, select a different encoder for URLs or JSON, and build readable multiline strings.
Official references: PHP's manual pages for string syntax and interpolation, strpos(), and htmlspecialchars().
Practice
Task: Safe HTML Greeting
Task
Start with this raw name:
$name = 'Amo & <Admin> "Lead"';
Create $safeName with htmlspecialchars($name, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') and print it inside an HTML paragraph.
Then prove the original value was not modified by printing it as plain CLI text on the next line.
Expected output begins:
<p>Hello, Amo & <Admin> "Lead"</p>
The second line must contain the original unescaped characters.
Hints
- Escape for HTML at output time.
- Assign the escaped result to a different variable.
- HTML entities are correct for the paragraph but not needed for plain CLI output.
Show solution
Solution
<?php
$name = 'Amo & <Admin> "Lead"';
$safeName = htmlspecialchars($name, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
echo "<p>Hello, $safeName</p>\n";
echo "Raw name: $name\n";
Explanation
htmlspecialchars() encodes the ampersand, angle brackets, and quotes for HTML text output. A browser displays them as text instead of interpreting angle brackets as markup.
The raw $name remains unchanged. Keeping raw data separate from context-specific output encoding prevents HTML entities from leaking into plain text, JSON, email, or later processing.
Task: Fix HTML Escaping
Task
Fix this code:
<?php
$comment = '<strong>Save & continue</strong>';
$safeComment = htmlspecialchars($comment, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
// Unsafe raw output:
echo "<div>$comment</div>\n";
// Incorrectly escaped a second time:
echo '<div>' . htmlspecialchars($safeComment, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') . "</div>\n";
Print one <div> whose content displays the original markup as text. The ampersand must appear normally in the browser, not as the literal text &.
Hints
- Escape the raw comment exactly once for this HTML context.
- Output
$safeComment, not$comment. - Do not pass the already escaped value through
htmlspecialchars()again.
Show solution
Solution
<?php
$comment = '<strong>Save & continue</strong>';
$safeComment = htmlspecialchars($comment, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
echo "<div>$safeComment</div>\n";
// HTML source:
// <div><strong>Save & continue</strong></div>
Explanation
Raw $comment must not be placed directly into HTML because its tags would be interpreted as markup. The escaped value is safe for this HTML text context.
Escaping $safeComment again would encode the entity ampersands, producing visible entity text such as < in the browser. Keep track of whether a variable is raw or encoded and escape once at the final output boundary.
Task: Build Message Summary
Task
Start with:
<?php
$template = 'Order {{id}} for {{name}}: {{items}}';
$name = ' Ada ';
$orderId = 'A-100';
$items = ['Workbook', 'Pen'];
Trim the name, join the item names with , , and replace all three placeholders with str_replace().
Then use strpos($message, 'Order') and a strict comparison with false to print whether the message starts with the expected word.
Expected output:
Order A-100 for Ada: Workbook, Pen
Starts with Order: yes
Hints
implode(', ', $items)creates the item text.- Supply matching search and replacement arrays to
str_replace(). - A match at the beginning has position
0, so do not use a truthiness check.
Show solution
Solution
<?php
$template = 'Order {{id}} for {{name}}: {{items}}';
$name = ' Ada ';
$orderId = 'A-100';
$items = ['Workbook', 'Pen'];
$itemSummary = implode(', ', $items);
$message = str_replace(
['{{id}}', '{{name}}', '{{items}}'],
[$orderId, trim($name), $itemSummary],
$template
);
$startsWithOrder = strpos($message, 'Order') === 0 ? 'yes' : 'no';
echo $message, PHP_EOL;
echo "Starts with Order: $startsWithOrder\n";
Explanation
The source values are normalised and assembled before placeholder replacement. str_replace() performs literal substitutions and returns the finished message.
strpos() returns integer 0 because Order begins at the first byte. Comparing with === 0 confirms the prefix; treating the result as a boolean would incorrectly interpret that valid position as false.