strpos($haystack, $needle) returns the position of the first $needle inside $haystack, counted from 0, or false if it is not there. Always test the result with !== false, because a match at the start returns 0.
If you only need a yes or no answer on PHP 8, str_contains() is clearer. Use strpos() when the position matters.
Syntax
strpos(string $haystack, string $needle, int $offset = 0): int|false
The search is case-sensitive. $offset tells it where to start looking; a negative offset counts from the end of the string.
The !== false pitfall
0 and false are equal under loose comparison (==). A match at position 0 is therefore treated as "not found" by any check that is not strict.
The same rule applies to stripos(), strrpos() and array_search(), which can all return 0 for a real match.
Case-insensitive search with stripos
stripos() takes the same arguments and ignores the case of ASCII letters.
Find the last occurrence with strrpos
strrpos() returns the position of the last match. It is the standard way to get a file extension or the last segment of a path.
For file names specifically, pathinfo($file, PATHINFO_EXTENSION) does this for you. Cutting the string at the position is covered on the substr() page.
Start the search at an offset
The third argument skips the beginning of the string. A negative offset starts that many characters before the end.
Find all occurrences
strpos() finds one match. Loop and move the offset past each match to find all of them:
Only the count? substr_count($text, 'the') is shorter. For patterns, preg_match_all() with PREG_OFFSET_CAPTURE returns every match with its position.
Japanese text: byte position vs character position
strpos() finds multibyte text correctly, but the number it returns is in bytes. Kana and common kanji take 3 bytes each in UTF-8, so in all-Japanese text the byte position is three times the character position, and in mixed text the two drift apart in less obvious ways. mb_strpos() returns characters, which is what mb_substr() expects.
Mixing the two is a classic bug: passing a strpos() result to mb_substr() starts too far into the string. Use strpos with substr, and mb_strpos with mb_substr.
Frequently Asked Questions
What does strpos() return in PHP?
The position of the first match as an integer, counted from 0, or false if the needle is not found. strpos('hello', 'l') returns 2.
Why do I need !== false with strpos()?
Because a match at the very start returns 0, and 0 == false is true. if (strpos($s, 'a')) treats a match at position 0 as "not found". Always compare with strpos($s, 'a') !== false.
What is the difference between strpos, stripos and strrpos?
strpos() finds the first match and is case-sensitive. stripos() finds the first match ignoring case. strrpos() finds the last match. strripos() finds the last match ignoring case.
Should I use strpos or str_contains to check if a string contains text?
On PHP 8 use str_contains($haystack, $needle), which returns a plain true or false and has no position 0 trap. Use strpos() when you need the position itself.
How do I use strpos with Japanese text?
strpos() finds Japanese text correctly but returns a byte position. If you pass the position to mb_substr() or show it to a user, use mb_strpos(), which returns the position in characters.