All tutorials

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.

phpsecurity

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

ScenarioUse
Need to know if substring exists (PHP 8+)str_contains()
Need the position of the substringstrpos() !== false
Case-insensitive searchstripos() !== false
Last occurrencestrrpos() !== 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.