Signature
mb_substr_count(string $haystack, string $needle, ?string $encoding = null): intPurpose
mb_substr_count() tells you how many times one string, the needle, appears inside another string, the haystack. Unlike plain byte-oriented helpers, it interprets both strings in a given character encoding, so the search is made in terms of characters. Use it when your text contains accented letters, non-Latin scripts or any other multibyte data.
Signature
Parameters and defaults
$haystack: the string that is searched.$needle: the substring to count. It must not be empty.$encoding: the name of the character encoding, such as"UTF-8". The default isnull, which means the internal character encoding is used. Passing the encoding explicitly makes your code independent of configuration.
The comparison is case-sensitive. There is no flag for ignoring case, and no offset or length parameter, so the whole haystack is always scanned.
Return value and failure cases
The function returns an int: the number of occurrences of $needle in $haystack. If nothing matches, the result is 0.
Since PHP 8.0 an invalid $encoding name throws a ValueError; older versions emitted a warning instead. An empty needle also raises a ValueError in PHP 8, so validate user-supplied needles before calling the function. Because the function is not documented to return false, you do not need a strict === false check on the result.
Example 1: the basics
The first example counts a short ASCII phrase and then a string made of multibyte characters, giving the encoding explicitly.
echo mb_substr_count("This is a test", "is"), "\n";
echo mb_substr_count("ÄÖÄÖÄ", "Ä", "UTF-8"), "\n";2 3
The word This contains is, and so does the standalone word is, hence two matches. The second line finds three occurrences of Ä.
Example 2: overlaps and case
Matches are counted without overlapping: after one match is found, scanning continues after its end. Matching is also case-sensitive, so normalize the text first if you want a case-insensitive count.
$overlap = mb_substr_count("aaaa", "aa", "UTF-8");
$sensitive = mb_substr_count("ÉCOLE école", "É", "UTF-8");
$lowered = mb_strtolower("ÉCOLE école", "UTF-8");
$folded = mb_substr_count($lowered, "é", "UTF-8");
echo json_encode([
"overlap" => $overlap,
"sensitive" => $sensitive,
"folded" => $folded,
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), "\n";{"overlap":2,"sensitive":1,"folded":2}
In aaaa the needle aa is counted twice, not three times. The capital É appears once in the original text, but after lowercasing both words contain é, so the folded count is two.
Example 3: handling errors
Because bad input throws, wrap calls that take external values in a try block. This helper returns either the count as a string or the exception class name.
function tryCount(string $needle, ?string $encoding): string {
try {
return (string) mb_substr_count("banana", $needle, $encoding);
} catch (ValueError $e) {
return get_class($e);
}
}
echo json_encode([
tryCount("an", "UTF-8"),
tryCount("", "UTF-8"),
tryCount("an", "bogus-encoding"),
]), "\n";["2","ValueError","ValueError"]
The first call finds an twice in banana. The empty needle and the unknown encoding name both produce a ValueError.
Edge cases
- A needle longer than the haystack simply yields
0. - An empty haystack with a non-empty needle yields
0. - Passing
nullas the encoding is the same as omitting it. - Strings that are not valid in the chosen encoding may give surprising counts, so validate input with
mb_check_encoding()when it comes from outside your program. - Visually identical text can be stored in different ways, for example a precomposed letter versus a base letter plus a combining mark. These are different character sequences and will not match each other unless you normalize them first.
Common mistakes
- Expecting overlapping matches. Counting
aainaaaareturns 2. If you need overlapping counts, loop withmb_strpos()and advance by one character. - Forgetting case sensitivity. Lowercase both strings with
mb_strtolower()before counting when case should not matter. - Passing an empty needle. Check
$needle !== ""first, or catchValueError. - Relying on the default encoding. The internal encoding depends on configuration. Passing
"UTF-8"explicitly makes the intent clear. - Mixing mb and non-mb functions. Positions and lengths from
strlen()are measured in bytes, while themb_functions work with characters. Do not combine their results.
Related functions
substr_count(): the byte-oriented counterpart, which also supports offset and length arguments.mb_strpos(): finds the position of the first occurrence of a substring.mb_substr(): extracts part of a multibyte string.mb_strlen(): returns the length of a string in characters.mb_strtolower(): normalizes case before a case-insensitive count.
Recap
mb_substr_count() returns an int with the number of non-overlapping, case-sensitive occurrences of a needle in a haystack, interpreted in the given encoding. Specify the encoding explicitly, never pass an empty needle, lowercase the text if case should be ignored, and catch ValueError when the inputs are not under your control.
Tested examples
echo mb_substr_count("This is a test", "is"), "\n";
echo mb_substr_count("ÄÖÄÖÄ", "Ä", "UTF-8"), "\n";2 3
$overlap = mb_substr_count("aaaa", "aa", "UTF-8");
$sensitive = mb_substr_count("ÉCOLE école", "É", "UTF-8");
$lowered = mb_strtolower("ÉCOLE école", "UTF-8");
$folded = mb_substr_count($lowered, "é", "UTF-8");
echo json_encode([
"overlap" => $overlap,
"sensitive" => $sensitive,
"folded" => $folded,
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), "\n";{"overlap":2,"sensitive":1,"folded":2}
function tryCount(string $needle, ?string $encoding): string {
try {
return (string) mb_substr_count("banana", $needle, $encoding);
} catch (ValueError $e) {
return get_class($e);
}
}
echo json_encode([
tryCount("an", "UTF-8"),
tryCount("", "UTF-8"),
tryCount("an", "bogus-encoding"),
]), "\n";["2","ValueError","ValueError"]