Multibyte strings7 min readExamples run on PHP 8.5.10

mb_substr_count()

Learn how PHP 8.4 mb_substr_count() counts non-overlapping occurrences of a substring in a multibyte string, with encoding rules, errors and examples.

Signature

mb_substr_count(string $haystack, string $needle, ?string $encoding = null): int
Available since PHP 4.3.07.48.08.18.28.38.48.5All versions →

Purpose

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 is null, 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.

Example
<?php
echo mb_substr_count("This is a test", "is"), "\n";
echo mb_substr_count("ÄÖÄÖÄ", "Ä", "UTF-8"), "\n";
Output
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.

Example
<?php
$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";
Output
{"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.

Example
<?php
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";
Output
["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 null as 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

  1. Expecting overlapping matches. Counting aa in aaaa returns 2. If you need overlapping counts, loop with mb_strpos() and advance by one character.
  2. Forgetting case sensitivity. Lowercase both strings with mb_strtolower() before counting when case should not matter.
  3. Passing an empty needle. Check $needle !== "" first, or catch ValueError.
  4. Relying on the default encoding. The internal encoding depends on configuration. Passing "UTF-8" explicitly makes the intent clear.
  5. Mixing mb and non-mb functions. Positions and lengths from strlen() are measured in bytes, while the mb_ functions work with characters. Do not combine their results.
  • 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

Basic counting with ASCII and multibyte text
<?php
echo mb_substr_count("This is a test", "is"), "\n";
echo mb_substr_count("ÄÖÄÖÄ", "Ä", "UTF-8"), "\n";
Output
2
3
Non-overlapping matches and case sensitivity
<?php
$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";
Output
{"overlap":2,"sensitive":1,"folded":2}
Catching ValueError for an empty needle or bad encoding
<?php
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";
Output
["2","ValueError","ValueError"]