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:
- Inspecting —
strlen(),str_contains(),str_starts_with(),str_ends_with() - Changing case —
strtolower(),strtoupper(),ucfirst(),ucwords() - Trimming whitespace —
trim(),ltrim(),rtrim() - Searching —
strpos(),strrpos(),str_contains() - Replacing —
str_replace(),substr_replace(),preg_replace() - Extracting / splitting —
substr(),str_split(),explode() - Joining / formatting —
implode(),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(), andstr_ends_with()overstrpos()when you only need a yes/no answer — they read more clearly and sidestep the0-vs-falsetrap entirely. - Use
trim()/ltrim()/rtrim()only to strip a set of individual characters (like whitespace); usesubstr()orstr_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 justtrim($text);on its own. - Use
sprintf()ornumber_format()for user-facing formatting instead of manually concatenating pieces with periods. - Escape any string bound for HTML output with
htmlspecialchars(); string functions likestr_replace()are not a substitute for output escaping.
Practice Exercises
- Exercise 1: Write a function
reverseWords(string $sentence): stringthat reverses the letters within each word but keeps the word order the same, e.g."PHP is fun"becomes"PHP si nuf". Hint: combineexplode(),strrev(), andimplode(). - 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()andstr_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()andsubstr()operate on bytes; use themb_*family for accurate results on multibyte UTF-8 text. - Internally, strings are
zend_stringstructures 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 position0is falsy under loose comparison. trim()/ltrim()/rtrim()take a character mask, not a substring — usestr_ends_with()plussubstr()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.
