PHP · Intermediate

Strings in PHP

10 min readUpdated September 24, 2026Every example verified

In short: PHP strings are sequences of bytes written in single quotes (taken literally), double quotes (escape sequences and $variable interpolation) or heredoc blocks. Functions such as strlen, substr, strpos, str_replace, str_contains and sprintf do most everyday work; for text outside ASCII, the mb_ functions count and slice characters instead of bytes.

Writing strings and pulling them apart

The quote style decides how much PHP does to the text. Inside single quotes only \' and \\ are special; everything else, including $name and \n, is kept as written. Inside double quotes \n, \t and \\ become newline, tab and backslash, and variables are expanded: "Hello $name". When the variable is followed by characters that could be part of a name, or when you need an array element or a property, wrap the expression in braces: "{$prices[$item]} each". A heredoc, <<<TXT ... TXT;, behaves like a long double-quoted string and keeps its line breaks; since PHP 7.3 the closing marker may be indented and that indentation is removed from every line. A nowdoc, <<<'TXT', is the single-quoted equivalent.

Strings are joined with . and extended with .=. They are immutable in the sense that every function returns a new string and leaves the original alone; strtoupper($s) does nothing to $s unless you assign the result back.

A string is a sequence of bytes. strlen counts bytes, substr($s, $start, $length) slices by byte offset (negative values count from the end), and strtoupper changes only ASCII letters. For ASCII text these are exactly what you want and they are fast. For text with accents, other alphabets or emoji, use the mb_ family from the mbstring extension, enabled on this site: mb_strlen, mb_substr, mb_strtoupper and, from PHP 8.4, mb_ucfirst and mb_trim. They count characters rather than bytes.

Searching: str_contains, str_starts_with and str_ends_with (PHP 8.0) return booleans and read clearly. strpos($haystack, $needle) returns the byte offset of the first match or false, and because the offset can be 0 you must test it with !== false. str_replace($search, $replace, $subject) replaces every occurrence and accepts arrays for bulk replacement. trim, ltrim and rtrim remove whitespace, or a set of characters you pass, from the ends. explode($separator, $s) splits into an array and implode($glue, $array) joins one. ucfirst, ucwords, strtolower and strtoupper change case; strrev reverses bytes; str_repeat and str_pad build padding.

Formatting is sprintf's job: %s for a string, %d for an integer, %.2f for a float with two decimals, %5d for right alignment in five columns and %-10s for left alignment in ten. printf prints instead of returning, and number_format($n, 2) adds thousands separators. Comparing strings with === is exact; strcmp returns negative, zero or positive for ordering and strnatcmp orders img2 before img10.

Syntax

 PHP · syntax
$a = 'literal $text\n';         // no interpolation, no escapes except \' and \\
$b = "expanded $name\n";        // interpolation and escapes
$c = "item: {$prices[$item]}";  // braces for expressions
$d = <<<TXT
    Multi-line text with $name
    keeps its line breaks.
    TXT;                          // closing indentation is stripped (PHP 7.3)

$s = $a . $b;   $s .= "more";
strlen($s);  mb_strlen($s);
substr($s, 0, 5);  substr($s, -3);  mb_substr($s, 0, 5);
str_contains($s, "x");  str_starts_with($s, "x");  strpos($s, "x");   // int or false
str_replace("-", "/", $s);  trim($s);  strtoupper($s);  ucfirst($s);  ucwords($s);
explode(",", $s);  implode(", ", $parts);  str_repeat("-", 20);  str_pad($s, 10, " ", STR_PAD_LEFT);
sprintf("%-10s %5d %8.2f", $name, $qty, $price);  number_format($n, 2);

strpos returns 0 for a match at the start; test with !== false. strlen and substr work in bytes, mb_strlen and mb_substr in characters.

Quotes, interpolation and heredoc

The same coffee order printed with single quotes, double quotes, braces and a heredoc.

 PHP
<?php
$item = "oat latte";
$size = "large";
$prices = ["oat latte" => 4.5];

echo 'Single quotes keep $item as is\n', "\n";
echo "Double quotes expand: $size $item\n";
echo "Braces for expressions: {$prices[$item]} each\n";
echo 'Concatenation ' . 'works ' . 'in ' . 'both' . "\n";

$order = <<<TXT
    Order summary
      $size $item
      Total: {$prices[$item]}
    TXT;
echo $order, "\n";

Output

Single quotes keep $item as is\n
Double quotes expand: large oat latte
Braces for expressions: 4.5 each
Concatenation works in both
Order summary
  large oat latte
  Total: 4.5

The first line prints $item and the two characters \n literally, because single quotes expand nothing; the line break comes from the separate "\n". The second line expands both variables. The third needs braces because $prices[$item] is an array lookup with a variable key. In the heredoc, the four spaces before the closing TXT are removed from every line, so the summary lines keep only their extra two spaces of indentation.

Searching, slicing and replacing

A warehouse code is inspected with the most common string functions.

 PHP
<?php
$code = "SHELF-B12-OAK";
echo strlen($code), "\n";
echo strtolower($code), "\n";
echo substr($code, 0, 5), "\n";
echo substr($code, -3), "\n";
var_dump(strpos($code, "B12"));
var_dump(strpos($code, "PINE"));
echo str_contains($code, "OAK") ? "oak shelf" : "other", "\n";
echo str_replace("-", "/", $code), "\n";
[$kind, $slot, $wood] = explode("-", $code);
echo ucfirst(strtolower($wood)), " in slot ", $slot, "\n";
echo str_starts_with($code, "SHELF") ? "shelf" : "not shelf", "\n";

Output

13
shelf-b12-oak
SHELF
OAK
int(6)
bool(false)
oak shelf
SHELF/B12/OAK
Oak in slot B12
shelf

substr with a negative start counts back from the end, so -3 gives the last three characters. strpos finds B12 at byte 6 and returns false, not -1, for a missing needle. explode splits on the hyphen and the array is unpacked straight into three variables. ucfirst(strtolower(...)) is the usual way to normalise a word's case.

Formatting a receipt and counting characters

printf lines up columns for a bakery receipt, then strlen and mb_strlen disagree about an accented name.

 PHP
<?php
$lines = [
    ["Sourdough loaf", 2, 3.9],
    ["Cinnamon knot", 3, 2.25],
    ["Cafe au lait", 1, 3],
];
$total = 0;
foreach ($lines as [$name, $qty, $price]) {
    $sub = $qty * $price;
    $total += $sub;
    printf("%-16s x%d %8.2f\n", $name, $qty, $sub);
}
echo str_repeat("-", 28), "\n";
printf("%-16s    %8.2f\n", "Total", $total);

$dessert = "Crème brûlée";
echo strlen($dessert), " bytes, ", mb_strlen($dessert), " characters\n";
echo mb_strtoupper($dessert), " | ", strtoupper($dessert), "\n";

Output

Sourdough loaf   x2     7.80
Cinnamon knot    x3     6.75
Cafe au lait     x1     3.00
----------------------------
Total                  17.55
15 bytes, 12 characters
CRÈME BRÛLÉE | CRèME BRûLéE

%-16s pads the name to 16 columns on the right, %d prints the quantity, and %8.2f right-aligns the subtotal in 8 columns with two decimals, so the numbers line up. The three accented letters each take two bytes in UTF-8, which is why strlen says 15 while mb_strlen says 12. strtoupper leaves those bytes untouched, producing the odd mixed case; mb_strtoupper understands the characters.

Common mistakes

  • Testing strpos with ! or == false

    Why it goes wrong: A match at the very start returns 0, and 0 == false is true, so if (!strpos($s, "a")) treats a found needle as missing.

    Fix: Compare with !== false, or use str_contains when you only need yes or no.

     PHP · fix
    <?php
    $s = "apple";
    if (strpos($s, "a") !== false) {
        echo "found\n";
    }
    if (str_contains($s, "a")) {
        echo "found again\n";
    }
  • Expecting single quotes to expand variables

    Why it goes wrong: 'Hello $name' prints the seven characters $name literally; only double quotes and heredocs interpolate.

    Fix: Use double quotes when the text contains variables, or concatenate with ..

  • Counting or slicing UTF-8 text with strlen and substr

    Why it goes wrong: They work in bytes. substr("Crème", 0, 3) cuts through the two-byte è and returns broken text, and strlen overstates the length.

    Fix: Use mb_strlen and mb_substr for any text that may contain non-ASCII characters.

  • Joining with + instead of .

    Why it goes wrong: "Room " + 12 throws TypeError in PHP 8; with two numeric strings, "1" + "2" quietly gives 3 instead of "12".

    Fix: The dot operator is the only way to concatenate: "Room " . 12.

Ways to write a string

FormInterpolationEscapesUse for
'text'no\' and \\ onlyfixed text, regular expressions
"text"yes\n \t \\ \$ \" and unicode \u{...}messages with variables
<<<TXT ... TXT;yesas double quotesmulti-line templates
<<<'TXT' ... TXT;nononemulti-line literal text

Where you use this

Two jobs come up constantly: parsing a line of input into fields, and producing aligned output. A line such as name=Ada;role=admin is broken apart with explode(";", $line) and then each piece with explode("=", $pair, 2), trimming as you go. Going the other way, a report with columns that line up is a printf format string with widths, and a table separator is str_repeat. Both directions reward knowing the byte-versus-character distinction as soon as names with accents appear.

 PHP · in practice
$fields = [];
foreach (explode(";", trim($line)) as $pair) {
    [$key, $value] = explode("=", $pair, 2);
    $fields[trim($key)] = trim($value);
}
printf("%-10s %s\n", "name", $fields["name"]);

Key points

  • Single quotes are literal; double quotes and heredocs expand $variables and escape sequences.
  • Use {$expr} braces for array elements and properties inside double quotes.
  • . concatenates; string functions return new strings and never modify their argument.
  • strpos returns an offset or false; check with !== false, or use str_contains.
  • strlen and substr count bytes; mb_strlen and mb_substr count characters.
  • explode splits, implode joins, str_replace substitutes, trim cleans the ends.
  • sprintf and printf align columns and fix decimals: %-10s, %5d, %.2f.

Try it yourself

The program reads a product code such as shelf-b12-oak and prints it unchanged. Make it print the code in upper case with every hyphen replaced by a slash.

Your program
<?php
$code = trim(fgets(STDIN));
echo $code, "\n";
Input the program receives: shelf-b12-oak
Expected output: SHELF/B12/OAK

Practise this

Open the PHP playground

Frequently asked questions

What is the difference between single and double quotes in PHP?

Single-quoted strings are taken literally: $name stays as those five characters and \n is a backslash followed by n. Double-quoted strings expand variables, support {$expression} and interpret escape sequences such as \n and \t. Neither is faster in any way you will notice; choose by whether you need interpolation.

How do I get the length of a UTF-8 string in PHP?

Use mb_strlen($s), which counts characters. strlen($s) counts bytes, and in UTF-8 an accented letter takes two bytes, most other scripts three, and emoji four. The same distinction applies to substr versus mb_substr and strtoupper versus mb_strtoupper.

Why does strpos return 0 and break my if statement?

0 means the needle was found at the first byte. Because 0 is false in a condition, if (strpos(...)) wrongly treats that as not found. Always write strpos(...) !== false, or use str_contains, str_starts_with and str_ends_with, which return proper booleans.

Progress is stored only in this browser.

How this page was checked. Every program on it was run with PHP 8.4 at build time by the publishing checks, and the output shown is what it printed. Running PHP inside the browser is not available yet, so the Run button is absent rather than pretending; copy the code and run it with PHP 8.4 locally.