Signature
mb_strpos(string $haystack, string $needle, int $offset = 0, ?string $encoding = null): int|falsePurpose
mb_strpos() answers the question "at which character does this text first appear?" It is the multibyte-aware counterpart of strpos(). Plain strpos() counts bytes, so in UTF-8 text a single accented letter, a CJK character or an emoji can shift every later position. mb_strpos() counts characters in the chosen encoding, starting at 0. That makes it the right tool when positions are shown to users or fed into other mb_* functions such as mb_substr().
Signature
Parameters and defaults
$haystack: the string to search in.$needle: the string to look for. Unlikestrpos(), a number is not treated as an ordinal character value, so pass text.$offset: where to start searching, measured in characters. The default is0. A negative offset counts from the end of the haystack.$encoding: the character encoding, for example"UTF-8". If it is omitted ornull, the internal encoding is used.
Passing the encoding explicitly keeps your code independent of configuration, which is useful in reusable code.
Return value and failures
The function returns the zero-based character position of the first match, or false when the needle is not found. An empty needle is accepted and matches at the start of the search, so mb_strpos($text, "") gives 0.
A ValueError is thrown if $offset is greater than the length of the haystack, and also if $encoding is not a valid encoding name.
Because position 0 is a valid result, always compare with false strictly.
Example 1: characters versus bytes
The word "café" starts at the same logical place in both calls, but the byte-based result is larger because "ë" occupies two bytes in UTF-8.
$text = "Zoë loves café";
echo json_encode([
"mb" => mb_strpos($text, "café", 0, "UTF-8"),
"bytes" => strpos($text, "café"),
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) . "\n";{"mb":10,"bytes":11}
The character position is 10, while the byte position is 11. If you used the byte value with mb_substr(), you would cut the text in the wrong place.
Example 2: finding repeated matches
To find the next match, search again with an offset just past the previous one. A missing needle yields false.
$text = "añb añb";
$first = mb_strpos($text, "ñ", 0, "UTF-8");
$second = mb_strpos($text, "ñ", $first + 1, "UTF-8");
$missing = mb_strpos($text, "z", 0, "UTF-8");
echo json_encode([$first, $second, $missing]) . "\n";[1,5,false]
The result is [1,5,false]. The offset is also in characters, so $first + 1 is safe regardless of how many bytes each character uses.
Example 3: negative offsets, empty needles and errors
A negative offset begins the search that many characters from the end. An offset beyond the string length is rejected with an exception.
$text = "日本語日本語";
$result = [
"negative" => mb_strpos($text, "本", -3, "UTF-8"),
"empty" => mb_strpos($text, "", 0, "UTF-8"),
];
try {
mb_strpos($text, "本", 7, "UTF-8");
} catch (ValueError $e) {
$result["error"] = "ValueError";
}
echo json_encode($result, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) . "\n";{"negative":4,"empty":0,"error":"ValueError"}
The string has six characters. Starting at -3 means starting at character 3, so the first match found is the second "本" at position 4. The offset 7 is greater than the length of 6, so the call throws.
Edge cases
- An offset equal to the string length is not larger than it, so it is allowed. It can only match an empty needle.
- Matching is case-sensitive. For case-insensitive searching, see
mb_stripos(). - The comparison is code-point based. Visually identical text can be stored in composed or decomposed Unicode forms and may not match.
- If the haystack contains bytes that are invalid for the declared encoding, results may not be what you expect, so validate input first when it comes from outside.
Common mistakes
- Using
if (mb_strpos(...)). A match at position 0 is falsy. Use!== false. - Mixing byte and character functions. Do not pass a result from
mb_strpos()tosubstr(), or one fromstrpos()tomb_substr(). - Forgetting the encoding. Relying on the internal encoding can make behavior depend on configuration. Pass
"UTF-8"when you mean UTF-8. - Passing a number as the needle and expecting a character code lookup. Numeric values are not treated that way here.
- Not guarding offsets. Offsets computed from user input can exceed the length and throw
ValueError.
Related functions
mb_strrpos()finds the last occurrence.mb_stripos()finds the first occurrence, ignoring case.mb_internal_encoding()gets or sets the default encoding.strpos()is the byte-based version, suitable for binary data or ASCII-only protocols.
Recap
mb_strpos() returns the zero-based character index of the first match or false. Pass the encoding explicitly, compare strictly with false, remember that offsets are character-based and may be negative, and expect a ValueError when the offset is too large or the encoding is invalid.
Tested examples
$text = "Zoë loves café";
echo json_encode([
"mb" => mb_strpos($text, "café", 0, "UTF-8"),
"bytes" => strpos($text, "café"),
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) . "\n";
{"mb":10,"bytes":11}
$text = "añb añb";
$first = mb_strpos($text, "ñ", 0, "UTF-8");
$second = mb_strpos($text, "ñ", $first + 1, "UTF-8");
$missing = mb_strpos($text, "z", 0, "UTF-8");
echo json_encode([$first, $second, $missing]) . "\n";
[1,5,false]
$text = "日本語日本語";
$result = [
"negative" => mb_strpos($text, "本", -3, "UTF-8"),
"empty" => mb_strpos($text, "", 0, "UTF-8"),
];
try {
mb_strpos($text, "本", 7, "UTF-8");
} catch (ValueError $e) {
$result["error"] = "ValueError";
}
echo json_encode($result, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) . "\n";
{"negative":4,"empty":0,"error":"ValueError"}