Strings

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

The reference material consulted while preparing these pages is listed in the section bibliography.

This page is written against PHP 8.5, checked on 2026-10-05. A PHP string is a sequence of bytes with no encoding attached; the functions on this page work on bytes, which is exactly right for ASCII and binary data and wrong for human text in other scripts. Character-aware processing with mbstring and intl is covered in Unicode, mbstring and intl, and regular expressions in Regular Expressions.

String literals

PHP has four ways to write a string literal.

Single-quoted

Everything is literal except \' (a quote) and \\ (a backslash). No variables are expanded and no other escapes are recognised, which makes single quotes the safe default for fixed text.

<?php

declare(strict_types=1);

$title = 'Kindred';
echo 'It\'s called $title\n', "\n";    // It's called $title\n  -- literally
echo 'C:\\Books\\', "\n";               // C:\Books\

Double-quoted

Double quotes recognise escape sequences and interpolate variables:

Escape Meaning

\n, \r, \t, \v, \f, \e

newline, carriage return, tab, vertical tab, form feed, escape (ESC)

\\, \$, \"

backslash, dollar sign, double quote

\0 to \777

a byte in octal

\x41

a byte in hexadecimal

\u{1F4DA}

the UTF-8 encoding of a Unicode code point (7.0)

<?php

declare(strict_types=1);

echo "Title:\tKindred\n";              // a real tab and newline
echo "Price: \$12\n";                  // literal dollar sign
echo "Shelf \u{1F4DA}\n";              // Shelf 📚
var_dump("\x41\101" === 'AA');         // bool(true)

Interpolation

In double-quoted strings and heredocs, PHP expands variables in two forms:

  • Simple syntax: $name, $array[key] (unquoted key, one level), $array[0], $object->property (one level).

  • Complex (curly) syntax: {$expression}, where the expression starts with $ and can be any variable, array access with quoted keys, property chain or method call. Use it whenever the simple form is ambiguous.

The third form, ${name} and ${expr}, is deprecated since PHP 8.2 and is scheduled for removal in PHP 9; write {$name} instead.

<?php

declare(strict_types=1);

$title = 'Kindred';
$book = ['title' => 'Dawn', 'author' => ['name' => 'Octavia E. Butler']];
$loan = new class { public string $member = 'M-17'; public function due(): string { return '2026-10-26'; } };

echo "Simple: $title, $book[title], $loan->member\n";
echo "Complex: {$book['author']['name']}, due {$loan->due()}\n";
echo "Plural: {$title}s\n";                     // braces separate the name from the following text
// echo "${title}";                             // Deprecated (8.2): Using ${var} in strings is deprecated
Simple: Kindred, Dawn, M-17
Complex: Octavia E. Butler, due 2026-10-26
Plural: Kindreds

Function calls, constants and static properties cannot be interpolated directly; assign them to a variable first or use sprintf().

Heredoc and nowdoc

A heredoc (<<<ID … ID) behaves like a double-quoted string over several lines, without needing to escape quotes. A nowdoc (<<<'ID' … ID) behaves like a single-quoted string. Since PHP 7.3 the closing marker may be indented, and that indentation is removed from every line (flexible heredoc); the closing marker no longer has to be followed by a newline either.

<?php

declare(strict_types=1);

$title = 'Kindred';
$year = 1979;

function card(string $title, int $year): string
{
    return <<<TXT
        Title: {$title}
        Year:  {$year}
          (first edition)
        TXT;                           // closing marker indentation (8 spaces) is stripped
}

echo card($title, $year), "\n";

$template = <<<'SQL'
    SELECT title FROM books WHERE year > :year   -- no interpolation: $year stays literal
    SQL;
echo $template, "\n";
Title: Kindred
Year:  1979
  (first edition)
SELECT title FROM books WHERE year > :year   -- no interpolation: $year stays literal

Mixing tabs and spaces in the indentation of a flexible heredoc is a parse error, as is a body line indented less than the closing marker.

Source: Heredoc and Nowdoc

String offsets

A single byte of a string is accessed with $s[$i]; negative offsets count from the end (7.1). Reading past the end yields a warning and an empty string. Writing $s[$i] = 'x' replaces one byte; assigning an empty string throws an Error. The old curly-brace syntax $s{0} was removed in PHP 8.0.

<?php

declare(strict_types=1);

$s = 'Solaris';
var_dump($s[0], $s[-1], $s[10] ?? 'none');   // "S" "s" "none"
$s[0] = 'P';
var_dump($s);                               // string(7) "Polaris"

try {
    $s[0] = '';
} catch (Error $e) {
    echo $e->getMessage(), "\n";            // Cannot assign an empty string to a string offset
}

Offsets are bytes: 'Łódź'[1] is half of a two-byte character. Use mb_substr() for characters.

The string API by family

The string extension (book.strings) has well over a hundred functions. The ones below are those used daily, grouped by purpose.

Searching

<?php

declare(strict_types=1);

$title = 'The Left Hand of Darkness';

// PHP 8.0: clear boolean checks
var_dump(str_contains($title, 'Hand'));         // bool(true)
var_dump(str_starts_with($title, 'The '));      // bool(true)
var_dump(str_ends_with($title, 'ness'));        // bool(true)

// Positions: strpos (case-sensitive), stripos (case-insensitive), strrpos (last occurrence)
var_dump(strpos($title, 'T'));                  // int(0)
var_dump(stripos($title, 'darkness'));          // int(17)
var_dump(strrpos('banana', 'a'));               // int(5)

// The strpos pitfall: position 0 is falsy
if (strpos($title, 'The') == false) {           // WRONG: strpos() returns 0, and 0 == false
    echo "BUG: 'The' reported as missing\n";    // this line runs
}
if (strpos($title, 'The') !== false) {          // RIGHT
    echo "found\n";
}

var_dump(strstr('reader@library.example', '@'));        // string(16) "@library.example"
var_dump(strstr('reader@library.example', '@', true));  // string(6) "reader"
var_dump(substr_count('banana', 'an'));                 // int(2)

strpos() returns int|false. Since a match at the very beginning returns 0, comparing with == or using the result in a boolean context reports "not found" for a string that starts with the needle. Use !== false, or better, str_contains() when you only need a yes/no answer. Note that str_contains($s, '') is always true.

Source: str_contains and strpos

Slicing

<?php

declare(strict_types=1);

$isbn = '978-0-00-000001-1';

var_dump(substr($isbn, 0, 3));      // string(3) "978"   start, length
var_dump(substr($isbn, -1));        // string(1) "1"     negative start: from the end
var_dump(substr($isbn, 4, -2));     // string(11) "0-00-000001" negative length: stop before the end
var_dump(substr($isbn, 100));       // string(0) ""      (8.0: empty string, no longer false)

Source: substr

Replacing

<?php

declare(strict_types=1);

// str_replace: search/replace, arrays allowed, optional count by reference
$clean = str_replace(['-', ' '], '', '978-0 00-000001-1', $count);
var_dump($clean, $count);                                // "9780000000011" int(4)

// Arrays are applied in order, so later replacements see earlier results:
var_dump(str_replace(['a', 'b'], ['b', 'c'], 'ab'));      // string(2) "cc"

// strtr with an array replaces the longest matches first and never re-scans replaced text:
var_dump(strtr('ab', ['a' => 'b', 'b' => 'c']));          // string(2) "bc"
var_dump(strtr('Hi all', ['Hi' => 'Hello', 'all' => 'everyone']));   // "Hello everyone"

var_dump(str_ireplace('KIN', 'Kin', 'KINdred'));          // case-insensitive: "Kindred"
var_dump(substr_replace('Hello', 'J', 0, 1));             // "Jello"

strtr() with an array is the right tool for template placeholders, because a replacement value can never be replaced again; chained str_replace() calls can.

Source: str_replace and strtr

Splitting and joining

<?php

declare(strict_types=1);

$csvLine = 'Kindred,Octavia E. Butler,,1979';

print_r(explode(',', $csvLine));            // empty fields are kept
print_r(explode(',', $csvLine, 2));         // limit: at most 2 pieces
print_r(explode(',', $csvLine, -1));        // negative limit: drop the last piece

echo implode(' | ', ['Solaris', 'Kindred']), "\n";   // Solaris | Kindred
print_r(str_split('ABCDEFGHIJ', 4));                 // ["ABCD", "EFGH", "IJ"]
echo wordwrap('The quick brown fox jumps', 10, "\n", true), "\n";

For real CSV, with quoted fields and embedded commas, use str_getcsv() or fgetcsv() instead of explode() (see Files, Streams and I/O). str_split() splits bytes; for characters use mb_str_split().

Source: explode, implode and str_split

Padding, trimming and repeating

<?php

declare(strict_types=1);

var_dump(str_pad('7', 3, '0', STR_PAD_LEFT));     // "007"
var_dump(str_pad('Title', 10, '.'));               // "Title....."
var_dump(trim("  Kindred \n"));                    // "Kindred"   strips " \n\r\t\v\0"
var_dump(rtrim('1.500', '0'));                     // "1.5"
var_dump(trim('[draft]', '[]'));                   // "draft"     custom character list
var_dump(trim('a..z', 'a..z'));                    // ".."        "a..z" is the RANGE a-z, not "a", ".", "z"
var_dump(str_repeat('=', 5));                      // "====="

The second argument of the trim() family is a list of characters, not a substring, and .. inside it denotes a range. PHP 8.4 added multibyte-aware mb_trim(), mb_ltrim() and mb_rtrim().

Source: str_pad and trim

Case

strtolower(), strtoupper(), ucfirst(), lcfirst() and ucwords() change the case of ASCII letters only. Since PHP 8.2 they are locale-insensitive (before, the result depended on setlocale()), so strtolower('ÉLAN') is 'Élan': the É is untouched. Use mb_strtolower() / mb_convert_case() for other letters.

<?php

declare(strict_types=1);

var_dump(ucwords('the left hand of darkness'));     // "The Left Hand Of Darkness"
var_dump(ucwords('jean-luc picard', " -"));         // "Jean-Luc Picard": custom delimiters
var_dump(strtolower('ÉLAN'), mb_strtolower('ÉLAN')); // "Élan" "élan"

Source: strtolower and ucwords

Comparison and similarity

strcmp() returns a negative number, 0 or a positive number (only the sign is meaningful: the magnitude is an implementation detail); strcasecmp() ignores ASCII case; strncmp()/strncasecmp() compare a prefix; strnatcmp()/strnatcasecmp() use natural order, in which img10 comes after img9. For locale-correct comparison of human text, use Collator from intl.

The similarity functions help with fuzzy search and "did you mean?":

  • levenshtein($a, $b) — the minimum number of single-byte insertions, deletions and substitutions (costs can be weighted);

  • similar_text($a, $b, $percent) — the number of matching characters, and the similarity as a percentage;

  • soundex($s) and metaphone($s) — phonetic keys for English names: words that sound alike get the same key.

<?php

declare(strict_types=1);

var_dump(strcmp('Dawn', 'Kindred') < 0, strcasecmp('KINDRED', 'kindred'));   // bool(true) int(0)

$files = ['img12.png', 'img10.png', 'img9.png'];
usort($files, strnatcmp(...));
echo implode(' ', $files), "\n";                     // img9.png img10.png img12.png

// "Did you mean?" for a mistyped title
$titles = ['Kindred', 'Solaris', 'Dawn', 'Neuromancer'];
$query = 'Solarsi';
$best = array_reduce($titles, fn (?string $carry, string $t): string =>
    $carry === null || levenshtein($query, $t) < levenshtein($query, $carry) ? $t : $carry);
echo "Did you mean {$best}?\n";                      // Did you mean Solaris?

var_dump(levenshtein('kitten', 'sitting'));          // int(3)
similar_text('World', 'Word', $percent);
printf("%.2f%%\n", $percent);                        // 88.89%
var_dump(soundex('Lem'), soundex('Lamb'));            // "L500" "L510"
var_dump(metaphone('Knight'));                        // "NFT"

All of these operate on bytes; levenshtein() on two UTF-8 strings counts byte edits. PHP 8.5 added grapheme_levenshtein() to the intl extension for character-level distance.

Formatting: printf, sprintf and number_format

printf() writes a formatted string, sprintf() returns it, fprintf() writes it to a stream, and vprintf()/ vsprintf() take the arguments as an array. A conversion specification is %[argnum$][flags][width][.precision]specifier:

Part Values

specifier

d integer, u unsigned, f/F float (locale-aware / not), e/E scientific, g/G shorter of f/e, s string, x/X hex, o octal, b binary, c character, % a literal percent

flags

- left-justify, + always show the sign, 0 pad with zeros, a space pads with spaces, 'c pads with character c

width

minimum number of characters

precision

digits after the decimal point for floats, maximum length for strings

argnum$

use argument number N (swap order, reuse arguments); write %2$s

<?php

declare(strict_types=1);

printf("[%5d] [%-5s] [%05.1f] [%x] [%b] [%'*8s] [%+d]\n", 42, 'ab', 3.14159, 255, 5, 'pad', 7);
// [   42] [ab   ] [003.1] [ff] [101] [*****pad] [+7]

echo sprintf('%2$s by %1$s', 'Octavia E. Butler', 'Kindred'), "\n";   // Kindred by Octavia E. Butler
echo vsprintf("%-10s %4d\n", ['Solaris', 1961]);                      // Solaris    1961

// number_format(number, decimals, decimal separator, thousands separator)
var_dump(number_format(1234567.891));                 // "1,234,568"
var_dump(number_format(1234567.891, 2));              // "1,234,567.89"
var_dump(number_format(1234567.891, 2, ',', '.'));    // "1.234.567,89"
var_dump(number_format(-1234.567, 2, '.', ' '));      // "-1 234.57"

var_dump(sprintf('%.2f', 1.005));                     // "1.00" -- 1.005 is really 1.00499999...

number_format() and %f round binary floats, so 1.005 formats as 1.00. For money, use integer minor units or BcMath\Number and format with NumberFormatter (intl), which also handles currency symbols and locales.

Source: sprintf and number_format

PHP has several functions for HTML. The important one is htmlspecialchars(), which converts &, <, >, " and ' to entities so that text can be safely placed in HTML. Since PHP 8.1 its default flags are ENT_QUOTES | ENT_SUBSTITUTE | ENT_HTML401, so single quotes are escaped and invalid UTF-8 is replaced with U+FFFD instead of returning an empty string; pass the flags and 'UTF-8' explicitly anyway, so the intent is visible:

<?php

declare(strict_types=1);

function e(string $text): string
{
    return htmlspecialchars($text, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
}

$review = '<script>alert("x")</script> Tom\'s & Jerry\'s';
echo e($review), "\n";
// &lt;script&gt;alert(&quot;x&quot;)&lt;/script&gt; Tom&#039;s &amp; Jerry&#039;s

echo nl2br(e("Line one\nLine two")), "\n";        // inserts <br /> before newlines
echo strip_tags('<b>bold</b> and <i>italic</i>', '<i>'), "\n";   // bold and <i>italic</i>
echo html_entity_decode('&eacute;t&eacute; &amp; hiver'), "\n";  // été & hiver
  • htmlentities() converts every character that has a named entity; with UTF-8 output it is unnecessary.

  • strip_tags() is not a sanitiser: it can be bypassed and keeps attributes on allowed tags. Escape on output instead; for user-supplied HTML that must be kept, use an HTML sanitiser library.

  • Escaping depends on the context (HTML body, attribute, URL, JavaScript). The full treatment, with the errata from older books, is in Forms, Validation and Output Escaping.

Common pitfalls

  • strpos() == false for a match at position 0 — use !== false or str_contains().

  • Byte functions on UTF-8 — strlen('Łódź') is 7, substr(), str_split(), strrev() and ucfirst() can cut or ignore multibyte characters.

  • trim() character lists — the second argument is a set, and .. defines a range.

  • Interpolating method calls with the simple syntax — "$obj->method()" interpolates only the property lookup, not the call; use {$obj->method()}.

  • str_replace() with arrays applies replacements sequentially; use strtr() for one-pass substitution.

  • Building SQL, shell commands or HTML by concatenation — use prepared statements, escapeshellarg() or argument arrays, and context-aware escaping.

Where older books and tutorials are outdated

  • $str{0} offset syntax was removed in 8.0; ${var} interpolation is deprecated since 8.2.

  • money_format() (Beak, Kromann) was removed in 8.0; use NumberFormatter::formatCurrency().

  • strpos($h, $n) !== false everywhere — still correct, but str_contains(), str_starts_with() and str_ends_with() (8.0) express the intent.

  • Locale-dependent case and float formatting — strtolower() and friends ignore the locale since 8.2, and float to string conversion since 8.0.

  • htmlspecialchars() defaults — books that call it without flags assume the pre-8.1 default, which did not escape single quotes.

  • utf8_encode()/utf8_decode() were deprecated in 8.2; use mb_convert_encoding().