PHP String Functions

Almost everything a PHP script does eventually touches text: reading form input, building HTML, formatting a price, or writing a log line. PHP ships with a huge standard library of built-in string functions for exactly this kind of work, so you rarely need to write your own text-processing logic from scratch. This lesson walks through how these functions are organized, how they behave internally, and how to use the most important ones correctly.

Overview: How PHP String Functions Work

A PHP string is simply a sequence of bytes. It can hold plain text, but it can also hold raw binary data (like part of an image) — PHP does not force an encoding on you. Because of this, the core string functions such as strlen(), substr(), and strtoupper() operate on bytes, not on “characters” in the human sense. For plain ASCII text (letters A-Z, digits, common punctuation) a byte and a character are the same thing, so this distinction rarely matters. For UTF-8 text containing accented letters, emoji, or non-Latin scripts, a single visible character can be made of two, three, or four bytes, and the byte-oriented functions will happily split a character in half. For that case PHP provides a parallel set of multibyte-safe functions in the mbstring extension, prefixed with mb_, such as mb_strlen() and mb_substr().

Unlike some languages, PHP does not attach string methods to a string object. Instead you call free-standing functions and pass the string in as an argument, for example strtoupper($text) rather than $text.toUpperCase(). A useful naming pattern to notice: functions starting with str_ generally work on a whole string or an array of strings (str_replace, str_contains, str_pad), while a leading i in an older-style function name usually means “case-insensitive” (stripos() is the case-insensitive twin of strpos()).

All of these functions are pure in the sense that they never modify the string you pass in — PHP strings are immutable from the script’s point of view. A function like trim() or str_replace() always returns a brand-new string (or an array of them), and if you want to keep the result you must assign it back to a variable: $text = trim($text);. It’s broadly useful to think of the built-in string toolkit in categories:

  • Inspectingstrlen(), str_contains(), str_starts_with(), str_ends_with()
  • Changing casestrtolower(), strtoupper(), ucfirst(), ucwords()
  • Trimming whitespacetrim(), ltrim(), rtrim()
  • Searchingstrpos(), strrpos(), str_contains()
  • Replacingstr_replace(), substr_replace(), preg_replace()
  • Extracting / splittingsubstr(), str_split(), explode()
  • Joining / formattingimplode(), sprintf(), number_format(), str_pad()

Syntax

Most string functions follow a similar pattern: the string (or the “haystack” to search in) is one of the first arguments, optional arguments come after with sensible defaults, and the function returns a new string, an array, an integer position, or false when something isn’t found. Here are the functions used most often, with their general shape:

Function What it does Example call
strlen Number of bytes in a string strlen($text)
strtolower / strtoupper Convert case of the whole string strtoupper($text)
ucfirst / ucwords Capitalize first letter of string / of each word ucwords($text)
trim / ltrim / rtrim Strip characters from both ends / left / right trim($text)
str_replace Replace all occurrences of a search value str_replace($search, $replace, $text)
substr Extract part of a string by offset and length substr($text, 0, 5)
strpos / str_contains Find a substring’s position / check if it exists str_contains($text, $needle)
explode / implode Split a string into an array / join an array into a string explode(‘,’, $text)
sprintf Build a formatted string from a template sprintf(‘Hi %s’, $name)
number_format Format a number with grouped thousands and decimals number_format($price, 2)
mb_strlen Character-safe length for multibyte (UTF-8) text mb_strlen($text)

Examples

Example 1: The everyday toolbox

<?php
$name = "  chris  ";
$clean = trim($name);

echo "Length: " . strlen($clean) . "\n";
echo "Uppercase: " . strtoupper($clean) . "\n";
echo "Title case: " . ucfirst($clean) . "\n";
echo sprintf("Hello, %s! You have %d new messages.\n", ucfirst($clean), 5);

Output:

Length: 5
Uppercase: CHRIS
Title case: Chris
Hello, Chris! You have 5 new messages.

This is the pattern you’ll use constantly: clean up user-entered text with trim() first, then transform it for display. Note that strlen() is measured after trimming, which is why it reports 5 rather than 9.

Example 2: Parsing and normalizing a list

<?php
$tagsInput = " php ,  web development,  Backend , api ";

$tags = explode(",", $tagsInput);
$tags = array_map(fn($tag) => ucwords(trim($tag)), $tags);
$clean = implode(", ", $tags);

echo $clean . "\n";
echo "Total tags: " . count($tags) . "\n";
echo "Contains 'Api': " . (in_array("Api", $tags) ? "yes" : "no") . "\n";

Output:

Php, Web Development, Backend, Api
Total tags: 4
Contains 'Api': yes

explode() splits the raw input on every comma, producing messy, inconsistently-spaced pieces. Running each piece through trim() and ucwords() inside array_map() normalizes them, and implode() glues the clean array back into one readable string.

Example 3: Building product slugs and labels

<?php
function makeSlug(string $title): string {
    $slug = strtolower(trim($title));
    $slug = preg_replace('/[^a-z0-9]+/', '-', $slug);
    return trim($slug, '-');
}

function summarize(string $title, float $price): string {
    $slug = makeSlug($title);
    $onSale = str_contains(strtolower($title), 'hub');
    $priceText = number_format($price, 2);
    $tag = $onSale ? '[Featured]' : '';
    return sprintf("%s ($%s) - /products/%s %s", $title, $priceText, $slug, $tag);
}

$products = [
    ["name" => "Wireless Mouse", "price" => 24.99],
    ["name" => "USB-C Hub, 7-in-1", "price" => 39.5],
];

foreach ($products as $product) {
    echo summarize($product["name"], $product["price"]) . "\n";
}

Output:

Wireless Mouse ($24.99) - /products/wireless-mouse 
USB-C Hub, 7-in-1 ($39.50) - /products/usb-c-hub-7-in-1 [Featured]

This example chains several functions to do real work: preg_replace() collapses any run of non-alphanumeric characters into a single dash to build a URL-friendly slug, str_contains() checks for a keyword to flag a product, number_format() guarantees two decimal places, and sprintf() assembles the final line without messy string concatenation.

Under the Hood: How PHP Handles Strings

Internally, the Zend Engine (PHP’s execution engine) represents every string as a zend_string structure: a block of memory that stores the byte length up front, the actual bytes, and a reference count. Storing the length up front is why strlen() is effectively instant even on huge strings — PHP doesn’t need to scan the whole string looking for a terminating character the way C does.

The reference count enables copy-on-write: when you assign one string variable to another ($b = $a;), PHP doesn’t duplicate the bytes immediately. Both variables point at the same underlying zend_string until one of them is modified, at which point PHP copies the data so the other variable is unaffected. This is why passing strings around, even large ones, is cheap.

When you call a function like str_replace() or substr(), PHP builds a brand-new zend_string for the result rather than editing the original in place — strings are treated as values, not as mutable buffers. Functions that accept an offset, like substr() and strpos(), also accept negative numbers, which PHP interprets as counting backward from the end of the string; substr($text, 0, -4) means “everything except the last 4 bytes.” Search functions such as strpos() return the zero-based byte offset of a match, or the boolean false if nothing is found — a detail that causes one of the most common bugs in PHP code, covered next.

Common Mistakes

Mistake 1: Comparing strpos() with ==

strpos() can legitimately return 0 (a match at the very start of the string), and in PHP, 0 == false evaluates to true. Using loose comparison silently treats a valid match at position 0 as “not found.”

<?php
$sentence = "PHP is fun to learn";
$pos = strpos($sentence, "PHP");

if ($pos == false) {
    echo "Not found";
} else {
    echo "Found at position $pos";
}

Output:

Not found

That output is wrong — “PHP” is clearly in the sentence, at position 0. The fix is to always use the strict comparison operator === (or !==) when checking the result of strpos(), so that 0 is never confused with false:

<?php
$sentence = "PHP is fun to learn";
$pos = strpos($sentence, "PHP");

if ($pos === false) {
    echo "Not found";
} else {
    echo "Found at position $pos";
}

Output:

Found at position 0

In modern PHP (8.0+) it’s often simpler still to avoid the numeric position entirely and use str_contains($sentence, "PHP"), which returns a clean boolean.

Mistake 2: Treating trim()/rtrim()’s second argument as a substring

The second argument to trim(), ltrim(), and rtrim() is not a substring to remove — it’s a character mask, a set of individual characters. PHP strips any of those characters from the end, one at a time, until it hits a character that isn’t in the set, no matter how many times that takes.

<?php
$filename = "phphp.php";
$name = rtrim($filename, ".php");
echo $name;

Output:

The character mask built from ".php" is just the set {'.', 'p', 'h'}. Every character in "phphp.php" happens to be one of those three, so rtrim() strips the entire string down to nothing instead of removing only the .php extension. This is an extreme case chosen to make the bug obvious, but the same mechanism causes subtler over-trimming on ordinary filenames too. To remove an exact trailing substring, check for it explicitly and use substr() (or preg_replace()) instead of a character mask:

<?php
$filename = "phphp.php";

if (str_ends_with($filename, ".php")) {
    $name = substr($filename, 0, -4);
} else {
    $name = $filename;
}

echo $name;

Output:

phphp

Best Practices

  • Always compare the result of strpos(), array_search(), and similar functions with === or !==, never ==.
  • Prefer str_contains(), str_starts_with(), and str_ends_with() over strpos() when you only need a yes/no answer — they read more clearly and sidestep the 0-vs-false trap entirely.
  • Use trim()/ltrim()/rtrim() only to strip a set of individual characters (like whitespace); use substr() or str_starts_with()/str_ends_with() to remove an exact prefix or suffix.
  • Switch to the mb_* functions (mb_strlen, mb_substr, mb_strtoupper) whenever you’re working with user-generated or multilingual UTF-8 text.
  • Remember string functions return new values — always reassign, e.g. $text = trim($text);, not just trim($text); on its own.
  • Use sprintf() or number_format() for user-facing formatting instead of manually concatenating pieces with periods.
  • Escape any string bound for HTML output with htmlspecialchars(); string functions like str_replace() are not a substitute for output escaping.

Practice Exercises

  • Exercise 1: Write a function reverseWords(string $sentence): string that reverses the letters within each word but keeps the word order the same, e.g. "PHP is fun" becomes "PHP si nuf". Hint: combine explode(), strrev(), and implode().
  • Exercise 2: You’re given a comma-separated string of emails with inconsistent spacing, e.g. " a@x.com,b@y.com , not-an-email , c@z.com". Write a function that returns only the entries that look like valid emails (contain an @), trimmed of whitespace, as a clean array.
  • Exercise 3: Write your own simplified slug function using only strtolower() and str_replace() (no regular expressions) that turns "Hello World!" into "hello-world". What edge cases does your version miss compared to a regex-based approach?

Summary

  • PHP string functions are free-standing (not methods), take the subject string as an argument, and always return a new value — they never modify the original string.
  • Core functions like strlen() and substr() operate on bytes; use the mb_* family for accurate results on multibyte UTF-8 text.
  • Internally, strings are zend_string structures with a stored length and reference counting, which is why length lookups are instant and simple assignment is cheap (copy-on-write).
  • Always use ===/!== when checking a search function’s result, since a valid match at position 0 is falsy under loose comparison.
  • trim()/ltrim()/rtrim() take a character mask, not a substring — use str_ends_with() plus substr() to strip an exact suffix.
  • Favor readable, modern functions (str_contains(), str_starts_with(), str_ends_with()) and formatting helpers (sprintf(), number_format()) over manual, error-prone string manipulation.