Tutorial
`strpos` in PHP: Stop Getting Bitten by the Zero-Position Bug
Why `strpos` returns false AND 0, and how to check correctly. A production-hardened guide to the classic PHP footgun.
strpos in PHP: Stop Getting Bitten by the Zero-Position Bug
strpos in PHP is deceptively simple on the surface, quietly destructive when misused. It’s a classic footgun, and I still see the mistake in code reviews. Let’s sort it out properly.
What strpos Does
strpos finds the position of the first occurrence of a substring within a string. The signature is straightforward:
strpos(string $haystack, string $needle, int $offset = 0): int|false
It returns the integer position of the first match, starting from zero. If no match is found, it returns false.
Simple example:
$text = 'Hello, world';
$pos = strpos($text, 'world');
var_dump($pos); // int(7)
The Bug
The natural instinct is to write:
if (strpos($text, 'world')) {
echo 'Found it';
}
This fails when the substring appears at position zero — the very start of the string. In that case, strpos returns 0, which is falsy in PHP:
$text = 'world domination';
if (strpos($text, 'world')) {
echo 'Found it'; // Never executes — strpos returns 0
}
The substring was found. strpos returned 0. But your condition was false.
The Fix: Strict Comparison with !== false
The correct pattern is to compare strictly against false:
$text = 'world domination';
if (strpos($text, 'world') !== false) {
echo 'Found it'; // Correctly executes
}
Use !== false (strict, type-safe) rather than != false (loose). With strict comparison, 0 (integer) and false (boolean) are distinct — which is what you need.
Practical Example: Detecting Hashtags
A common real-world use:
function containsHashtag(string $text): bool
{
return strpos($text, '#') !== false;
}
$post = '#php is still relevant in 2024';
if (containsHashtag($post)) {
echo 'Post contains a hashtag';
}
Wrapping the check in a typed function keeps calling code clean and removes the !== false noise from every call site.
PHP 8+: str_contains for Simple Checks
If you’re on PHP 8 or above, use str_contains for simple existence checks:
if (str_contains($text, '#')) {
echo 'Contains a hashtag';
}
str_contains returns a plain boolean, so there’s no zero-false ambiguity. Use it when you only need to know whether the substring exists.
Use strpos when you need the position:
$pos = strpos($text, '#');
if ($pos !== false) {
$tag = substr($text, $pos);
echo "Hashtag starts at position {$pos}: {$tag}";
}
Watch Out in switch Statements
The same zero-false confusion appears in switch blocks, which use loose equality by default:
switch (strpos($text, 'world')) {
case false:
echo 'Not found';
break;
case 0:
echo 'Found at start';
break;
}
With loose comparison, 0 == false is true, so the false case matches both “not found” and “found at position zero”. Restructure the logic or use match (PHP 8+), which uses strict comparison:
$result = match(true) {
strpos($text, 'world') === false => 'Not found',
strpos($text, 'world') === 0 => 'Found at start',
default => 'Found elsewhere',
};
Quick Reference
| Scenario | Use |
|---|---|
| Need to know if substring exists (PHP 8+) | str_contains() |
| Need the position of the substring | strpos() !== false |
| Case-insensitive search | stripos() !== false |
| Last occurrence | strrpos() !== false |
| Multibyte strings (UTF-8) | mb_strpos() !== false |
Summary
strpos is reliable and fast, but its return type requires strict handling. Always use !== false when checking the result in a conditional. On PHP 8+, reach for str_contains when position doesn’t matter. Stay alert to loose comparison traps in switch blocks — the same zero-equals-false confusion applies there too.