mb_strpos
Finds the character position of the first occurrence of a substring in a multibyte string.
mb_strpos(string $haystack, string $needle, int $offset = 0, ?string $encoding = null): int|false- Added in PHP 4.0.6
Parameters
| Parameter | Type | Description |
|---|---|---|
$haystack | string | The string to search in. |
$needle | string | The string to search for. May be empty as of PHP 8.0. |
$offsetoptional | int= 0 | Character position to start searching from. Negative counts back from the end of the haystack. |
$encodingoptional | ?string= null | Character encoding. Null uses the internal encoding. |
Return value
int|falseThe character position of the first occurrence, counted from 0, or false if the needle is not found.
Errors
Throws a ValueError if offset is greater than the length of haystack, or if encoding is not a valid encoding.
Changelog
- PHP 8.0.0needle now accepts an empty string.
- PHP 8.0.0encoding is nullable now.
- PHP 7.1.0Support for negative offsets was added.
mb_strpos() is the character-counting version of strpos(). Both find the first occurrence of a needle; the difference is the unit of the answer. In 'Bäckerei Straße' the word Straße starts at character 9 but at byte 10, because ä costs two bytes. Whichever number you get, you must pair it with a matching slicer: character offsets go to mb_substr(), byte offsets go to substr().
When to reach for it
Use it when you need the location of something so you can cut, measure or highlight around it: splitting a line at the first colon, finding where a token begins to wrap it in markup, or checking that a prefix sits at position 0. If you only want a yes/no answer, str_contains() is clearer and does not have the falsy-zero problem.
What trips people up
Two things. First, the classic: a match at the start returns 0, so if (mb_strpos(...)) reports "not found" for the most obvious match there is. Use !== false.
Second, an offset past the end of the haystack throws a ValueError rather than returning false, which turns a sloppy loop over successive matches into a fatal error.
Example
When to use it
- Find where a delimiter sits so you can split a line into a key and a value
- Locate a search term in accented text to wrap it in highlight markup
- Walk through every occurrence of a token by feeding the previous position back as the offset
Frequently asked questions
What is the difference between strpos and mb_strpos?
strpos returns a byte offset and mb_strpos returns a character offset. They agree only for ASCII, and mixing a character offset into substr() or a byte offset into mb_substr() corrupts the result.
Why does mb_strpos return 0 and my if statement treat it as not found?
0 is a valid position — it means the needle is at the very start — but PHP evaluates it as falsy. Compare the return value with !== false rather than testing it directly.
Is mb_strpos case-sensitive?
Yes. Use mb_stripos for a case-insensitive search, which also folds accented and non-Latin case pairs correctly.
More examples
Splitting a line at the first delimiter
mb_strpos gives a character index, so both mb_substr calls line up correctly even though 'ß' and 'ö' are two bytes each. Swapping in substr() here would cut in the wrong place and split a character.
<?php
mb_internal_encoding('UTF-8');
$line = 'Straße: Königsallee 42';
$pos = mb_strpos($line, ':');
if ($pos !== false) {
echo 'key: ', mb_substr($line, 0, $pos), "\n";
echo 'value: ', ltrim(mb_substr($line, $pos + 1)), "\n";
}Finding every occurrence
Advancing by mb_strlen('año') rather than by the byte length keeps the offsets in character units, producing 0, 4 and 8. Note the loop increments before searching again — passing an offset past the end of the string would throw a ValueError.
<?php
mb_internal_encoding('UTF-8');
$s = 'año año año';
$pos = 0;
$found = [];
while (($pos = mb_strpos($s, 'año', $pos)) !== false) {
$found[] = $pos;
$pos += 3;
}
print_r($found);
Discussion