Lesson 5 of 20

Strings

Single Quotes and Double Quotes Are Not the Same

PHP treats the two quote characters differently, and this is not a style choice. A double-quoted string is scanned by PHP: variables inside it are replaced with their values, and escape sequences such as \n for a newline and \t for a tab are interpreted. A single-quoted string is taken literally, character for character. The only two escapes it understands are \' for a quote and \\ for a backslash.

Replacing a variable's name with its value inside a string is called interpolation, and it usually reads better than a chain of dots. PHP will happily interpolate a simple variable and a simple array key, but for anything with more structure — an associative key in quotes, a property of an object, a method call — you need braces around the expression. When interpolation mysteriously prints half of what you wanted, missing braces is nearly always the reason.

Which should you use? Double quotes when you are interpolating, single quotes otherwise. The reason is readability, not speed: any performance difference between them is far too small to measure in a real application, and you will see confident claims online to the contrary that are simply outdated folklore.

One genuinely practical use for single quotes: writing text that itself contains dollar signs, such as a rupee-free price template or a regular expression. In single quotes, 'Cost is $total' prints exactly that, with no attempt to look up a variable.

Example
<?php
$name  = "Ananya";
$marks = ['physics' => 91, 'maths' => 88];

// Double quotes: interpolation and escapes
echo "Hello, $name!\n";              // Hello, Ananya! + newline
echo "Physics: {$marks['physics']}";  // braces needed for a quoted key
echo "Total: {$marks['physics']}/100";

// Single quotes: exactly what you typed
echo 'Hello, $name!\n';   // Hello, $name!\n   (literal, no newline)

// Braces also disambiguate where a name ends
$item = "book";
echo "I bought 3 {$item}s";   // "I bought 3 books"
// echo "I bought 3 $items";   // looks for $items - undefined!

// Escaping a quote inside the same kind of quote
echo "She said \"hello\"";
echo 'It\'s fine';
Notes
  • \n inside a double-quoted string is a real newline character, but a browser collapses newlines in HTML. If you want a visible line break on a web page you need <br>, or nl2br() to convert newlines from a textarea into <br> tags. \n is for files, logs and command-line output.

Heredoc and Nowdoc for Long Text

When a string spans many lines — an email body, a block of HTML, an SQL query — quoting gets ugly fast, because every quote inside has to be escaped. Heredoc syntax solves this. You write <<< followed by an identifier of your choosing, then your text on the following lines, then the same identifier again to close.

Heredoc behaves like a double-quoted string: variables are interpolated, and you never have to escape a quote. Nowdoc is its literal twin — write the opening identifier in single quotes and nothing inside is interpolated, which is what you want for text that legitimately contains dollar signs.

Since PHP 7.3 the closing identifier can be indented to match the surrounding code, and whatever indentation you give it is stripped from every line of the body. That is what makes heredoc usable inside a method without wrecking your file's indentation. The rule to remember is that no line of the body may be indented less than the closing identifier, or you get a parse error.

A caution worth stating plainly, because heredoc invites it: this is a fine way to build an email or an HTML block, and a terrible way to build an SQL query with user data pasted into it. Interpolating $email into a query string is SQL injection, no matter how tidy the syntax looks. The database lessons show the correct approach, which is placeholders and prepared statements.

Example
<?php
$name  = "Ananya";
$total = 4999;

// Heredoc - interpolates, no escaping needed
$email = <<<TEXT
    Dear $name,

    Your order has been confirmed.
    Amount paid: Rs $total

    Thank you,
    Campus Store
    TEXT;

echo $email;

// Nowdoc - completely literal, note the quoted identifier
$template = <<<'TPL'
    Hello $name, your balance is $amount.
    TPL;

echo $template;   // prints the dollar signs as text

// Heredoc is fine for HTML
$card = <<<HTML
    <div class="card">
      <h3>{$name}</h3>
      <p>Paid: Rs {$total}</p>
    </div>
    HTML;
Notes
  • Never build an SQL query with heredoc interpolation. <<<SQL SELECT * FROM users WHERE email = '$email' SQL is exactly the injection hole prepared statements exist to close — the neat formatting just makes it look more professional than it is.

The String Functions You Will Reach For Daily

PHP's string library is enormous, but a small subset does almost all the work in a normal project. Learn these first and look the rest up when a specific need arises.

trim() deserves special mention because it belongs on nearly every piece of form input you ever touch. Users paste values with trailing spaces constantly, and an email address of "ananya@example.com " will fail validation, fail a database lookup, and give you a bug report that says "it works on my phone". Trim first, then validate.

explode() and implode() are the pair that converts between a string and an array. Splitting a comma-separated list of tags into an array, working with it, and joining it back is one of the most common small tasks in a CMS. Note the argument order is inconsistent between them — explode(separator, string) but implode(separator, array) — which is a historical wart worth memorising rather than fighting.

str_replace() does a plain literal replacement and accepts arrays for both the search and the replacement, which makes it a quick way to fill a template. It is not a regular-expression function, so special characters in your search string have no special meaning — usually exactly what you want.

Example
<?php
$text = "  Introduction to Server-Side PHP  ";

echo trim($text);                  // "Introduction to Server-Side PHP"
echo strlen(trim($text));          // 31
echo strtoupper("php");            // "PHP"
echo ucfirst("welcome");           // "Welcome"
echo ucwords("ananya sharma");     // "Ananya Sharma"
echo substr("Introduction", 0, 5); // "Intro"
echo str_repeat("-", 30);          // a divider line
echo str_pad("7", 4, "0", STR_PAD_LEFT);  // "0007" - invoice numbers

// Splitting and joining
$tags = "php, mysql, security";
$list = array_map('trim', explode(',', $tags));
// ['php', 'mysql', 'security']
echo implode(' | ', $list);        // "php | mysql | security"

// Replacing
echo str_replace('World', 'PHP', 'Hello, World!');   // "Hello, PHP!"

// Filling a template in one call
$out = str_replace(
    ['{name}', '{course}'],
    ['Ananya', 'PHP Basics'],
    'Hi {name}, welcome to {course}.'
);

// Turning a URL slug into a title
$slug = 'working-with-mysql';
echo ucwords(str_replace('-', ' ', $slug));   // "Working With Mysql"
Notes
  • trim() takes an optional second argument listing which characters to strip, which makes it useful beyond whitespace: trim($path, '/') removes slashes from both ends of a URL path, and rtrim($dir, '/\\') normalises a folder path before you append a filename.

The strpos Trap, and What Replaced It

strpos() finds the position of one string inside another and returns that position as a number — or false if it is not there at all. The trap is immediate: if the text is found right at the beginning, the position is 0, and 0 is falsy. So if (strpos($email, 'a')) is false both when the letter is missing and when it is the very first character.

This is one of the most reported beginner bugs in PHP's entire history. The historically correct fix is to compare strictly: strpos($haystack, $needle) !== false. Notice that !== false is essential — != false would fall straight back into the same trap, since 0 != false is itself false.

PHP 8 finally removed the need for any of that. str_contains() returns a plain boolean, and str_starts_with() and str_ends_with() cover the two most common specific cases. They say what they mean, they cannot be misread, and there is no reason to use strpos() for a yes-or-no question ever again. You will still meet the old pattern in existing code, which is why it is worth being able to recognise.

Keep strpos() for the case it is actually for: when you genuinely need to know where something is, usually so you can slice the string with substr().

Example
<?php
$email = "ananya@example.com";

// The bug
if (strpos($email, 'a')) {
    echo "found";     // never runs - 'a' is at position 0
}

// The historic fix
if (strpos($email, 'a') !== false) {
    echo "found";     // correct
}

// PHP 8: say what you mean
var_dump(str_contains($email, '@'));           // true
var_dump(str_starts_with($email, 'ananya'));   // true
var_dump(str_ends_with($email, '.com'));       // true

// A real check on an uploaded filename
$file = 'notes.pdf';
if (!str_ends_with(strtolower($file), '.pdf')) {
    echo "Only PDF files are accepted.";
}

// strpos still earns its place when you need the position
$domain = substr($email, strpos($email, '@') + 1);
echo $domain;    // "example.com"
Notes
  • The same === false care applies to array_search(), which returns the key it found — and a valid key can be 0. Any PHP function that returns "a value, or false on failure" needs strict comparison.

Bytes Versus Characters: Why strlen Lies

strlen() does not count characters. It counts bytes. For plain English text those are the same number, so the problem stays invisible until the day someone types their name in Hindi, Odia, Tamil or Bengali — or simply uses an emoji or a curly apostrophe pasted from Word.

Modern web pages use UTF-8, an encoding in which a character takes between one and four bytes. Latin letters take one, most Indian-language characters take three, and many emoji take four. So strlen("नमस्ते") reports 18, not 6. If you use that number to enforce a name-length limit, you have just told a large number of your users that their own name is too long.

The fix is the mb_ family — mb_strlen(), mb_substr(), mb_strtoupper() and friends — which understand character encodings. They come from the mbstring extension, which is enabled by default on most hosts and which you should confirm is on before relying on it.

The most damaging version of this bug is truncation. substr() cuts at a byte position, so cutting a UTF-8 string in the middle of a three-byte character leaves half a character behind, and the browser renders a replacement glyph. Any "read more" preview built with substr() will eventually produce visible garbage on a non-English post. Use mb_substr() for anything a human will read.

One more member of the family to distrust: strrev() reverses bytes, which destroys multibyte text completely. There is no mb_strrev(); reversing text correctly needs mb_str_split() first. In practice, reversing a string is rarely something a real application needs.

Example
<?php
$english = "Hello";
$hindi   = "नमस्ते";

echo strlen($english);      // 5
echo mb_strlen($english);   // 5   - same for plain ASCII

echo strlen($hindi);        // 18  - bytes
echo mb_strlen($hindi);     // 6   - characters

// Truncation done wrong and right
$post = "नमस्ते दुनिया";
echo substr($post, 0, 5);      // cuts mid-character - broken output
echo mb_substr($post, 0, 5);   // clean 5 characters

// A safe excerpt helper
function excerpt(string $text, int $limit = 120): string {
    $text = trim($text);
    if (mb_strlen($text) <= $limit) {
        return $text;
    }
    return mb_substr($text, 0, $limit) . '...';
}

// Case conversion for non-English text
echo mb_strtoupper("café");   // "CAFÉ"
echo strtoupper("café");      // "CAFé"  - the accent is untouched
Notes
  • Make UTF-8 the default everywhere and this whole class of problem disappears: <meta charset="utf-8"> in your HTML, utf8mb4 as the MySQL charset on your connection and columns, and 'UTF-8' as the third argument to htmlspecialchars(). Mixing encodings between these three is how question marks and diamond symbols appear in a database.

Formatting Output for People

Raw values rarely look right on a page. number_format() adds thousands separators and fixes the number of decimal places, which is what you want for prices, totals and marks. sprintf() builds a formatted string from a template and returns it; printf() is the same thing but prints directly.

sprintf() is worth learning properly because it keeps a sentence readable. Compare a chain of five concatenations against one template string with %s and %d placeholders — the second version lets you see the sentence. The common placeholders are %s for a string, %d for a whole number, %.2f for two decimal places, and %% for a literal percent sign. Adding a width, as in %05d, pads with zeros, which is how you turn order id 42 into ORD-00042.

One honest limitation for Indian projects: number_format() groups digits in threes, so 1234567 becomes 1,234,567 and not the lakh-and-crore grouping of 12,34,567. PHP's core has no built-in Indian grouping. If the intl extension is available on your server, the NumberFormatter class with the en_IN locale will do it; otherwise you write a small helper of your own. It is better to know this now than to discover it during a demo.

Finally, remember that formatting is a display concern. Format at the moment you print, and keep the raw number in your variables and your database. A price stored as the string "1,234.00" cannot be added, sorted or compared, and untangling that later is genuinely painful.

Example
<?php
$total = 1234567.891;

echo number_format($total, 2);            // "1,234,567.89"
echo number_format($total, 0, '.', '');    // "1234568"  - no grouping

// sprintf keeps the sentence readable
$name = "Ananya";
$got  = 87;
$max  = 100;

echo sprintf('%s scored %d out of %d (%.1f%%)', $name, $got, $max, $got / $max * 100);
// "Ananya scored 87 out of 100 (87.0%)"

// Zero-padded ids
echo sprintf('ORD-%05d', 42);   // "ORD-00042"

// printf prints instead of returning
printf('Balance: Rs %s', number_format(4999.5, 2));

// Wrapping long text
echo wordwrap("A very long line of feedback from a student", 20, "\n", true);

// Turning textarea line breaks into HTML - escape first, then convert
$comment = $_POST['comment'] ?? '';
echo nl2br(htmlspecialchars($comment, ENT_QUOTES, 'UTF-8'));
Notes
  • Order matters in that last line. Escape with htmlspecialchars() first and run nl2br() afterwards. Doing it the other way round means your freshly inserted <br> tags get escaped into visible text — and, worse, encourages the habit of escaping before you have finished assembling the output.
Ask AI