Multibyte strings7 min readExamples run on PHP 8.5.10

mb_stripos()

Learn how mb_stripos() finds the first case-insensitive match in a multibyte string, how offsets and encodings work, and how to avoid the false-versus-0 trap.

Signature

mb_stripos(
    string $haystack,
    string $needle,
    int $offset = 0,
    ?string $encoding = null
): int|false
Available since PHP 5.2.07.48.08.18.28.38.48.5All versions →

Purpose

mb_stripos() answers a simple question: where does the first occurrence of a piece of text begin inside a larger string, ignoring letter case? Unlike stripos(), it counts characters in the chosen encoding, not bytes. That makes it the right tool for UTF-8 text such as Café, Ünal or Crème Brûlée, where one visible letter can take more than one byte.

Its case-sensitive sibling is mb_strpos(). If you only deal with plain ASCII, stripos() is enough. Once accented or non-Latin letters appear, the multibyte version gives positions that match what a reader sees.

Signature

Parameters and defaults

  • $haystack is the string to search in.
  • $needle is the text to look for. Since PHP 8.0 an empty needle is accepted.
  • $offset is the character position at which the search starts. It defaults to 0. A negative offset counts back from the end of the haystack.
  • $encoding names the character encoding, for example 'UTF-8'. If it is omitted or null, the internal encoding setting is used.

Passing the encoding explicitly makes your code independent of configuration, which is useful when the same function runs in different environments.

Return value and failure cases

The function returns the zero-based character position of the first match as an int, or false when the needle does not occur.

Two situations raise a ValueError:

  • the $offset is greater than the length of the haystack;
  • the $encoding is not a valid encoding name (this has been an exception since PHP 8.0; before that it was a warning).

A needle that is merely absent is not an error; you get false.

Example 1: accented letters, ignoring case

The positions below are character positions. The needle BRÛLÉE is written in capitals, yet it matches Brûlée in the haystack.

Example
<?php
$text = 'Crème Brûlée and Café';
echo json_encode([
    mb_stripos($text, 'BRÛLÉE', 0, 'UTF-8'),
    mb_stripos($text, 'café', 0, 'UTF-8'),
    mb_stripos($text, 'tea', 0, 'UTF-8'),
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), "\n";
Output
[6,17,false]

The third lookup finds nothing, so it yields false.

Example 2: using an offset

The offset lets you skip earlier matches. A positive value starts at that character index; a negative value starts that many characters before the end.

Example
<?php
$text = 'Tea, tea, TEA';
echo json_encode([
    mb_stripos($text, 'tea', 1, 'UTF-8'),
    mb_stripos($text, 'tea', 6, 'UTF-8'),
    mb_stripos($text, 'tea', -3, 'UTF-8'),
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), "\n";
Output
[5,10,10]

Starting at index 1 skips the first Tea and finds the lowercase one at 5. Starting at 6 or at -3 lands on the final uppercase TEA at 10.

Example 3: strict checks, empty needle and errors

A match at the very start returns 0, which is falsy. Always compare with === or !== against false.

Example
<?php
function findWord(string $haystack, string $needle): string
{
    $position = mb_stripos($haystack, $needle, 0, 'UTF-8');
    return $position === false ? 'missing' : 'at ' . $position;
}

echo findWord('Alpha', 'al'), "\n";
echo findWord('Alpha', 'z'), "\n";
echo json_encode(mb_stripos('abc', '', 0, 'UTF-8')), "\n";
try {
    mb_stripos('abc', 'a', 4, 'UTF-8');
} catch (ValueError $e) {
    echo get_class($e), "\n";
}
Output
at 0
missing
0
ValueError

The empty needle matches immediately at position 0. The offset 4 is beyond the three-character haystack, so a ValueError is thrown.

Edge cases

  • Offset equal to the length is not larger than the haystack, so it does not trigger the offset error; only values greater than the length do.
  • Negative offsets have been supported since PHP 7.1. A negative offset that reaches past the start is not the same as a too-large positive offset, so test your boundaries.
  • Mismatched encoding: if the bytes are not valid in the declared encoding, positions can be misleading. Make sure the string and the $encoding argument agree.
  • Case rules are those implemented by the mbstring extension for the given encoding. For ordinary letters in common scripts they behave as expected, but avoid relying on unusual case mappings for logic that must be exact.

Common mistakes

  1. Writing if (mb_stripos($s, $n)) and treating a match at index 0 as a failure. Use !== false.
  2. Mixing byte offsets from strpos() or strlen() with character offsets from mb_stripos(). Stay inside the mb_* family when slicing, for example with mb_substr().
  3. Omitting the encoding and assuming a default that differs between environments.
  4. Using stripos() on UTF-8 text and then wondering why accented capitals are not matched or positions are too large.
  5. Forgetting that a large offset throws instead of quietly returning false.
  • mb_strpos() for the case-sensitive version.
  • stripos() and strpos() for byte-based searching.
  • mb_substr() to extract text using the position you found.

Recap

mb_stripos() returns the character index of the first case-insensitive match, or false. Give it an explicit encoding, compare the result strictly with false, and keep the offset within the haystack length to avoid a ValueError.

Tested examples

Case-insensitive search with accented letters
<?php
$text = 'Crème Brûlée and Café';
echo json_encode([
    mb_stripos($text, 'BRÛLÉE', 0, 'UTF-8'),
    mb_stripos($text, 'café', 0, 'UTF-8'),
    mb_stripos($text, 'tea', 0, 'UTF-8'),
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), "\n";
Output
[6,17,false]
Positive and negative offsets
<?php
$text = 'Tea, tea, TEA';
echo json_encode([
    mb_stripos($text, 'tea', 1, 'UTF-8'),
    mb_stripos($text, 'tea', 6, 'UTF-8'),
    mb_stripos($text, 'tea', -3, 'UTF-8'),
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), "\n";
Output
[5,10,10]
Strict comparison, empty needle and ValueError
<?php
function findWord(string $haystack, string $needle): string
{
    $position = mb_stripos($haystack, $needle, 0, 'UTF-8');
    return $position === false ? 'missing' : 'at ' . $position;
}

echo findWord('Alpha', 'al'), "\n";
echo findWord('Alpha', 'z'), "\n";
echo json_encode(mb_stripos('abc', '', 0, 'UTF-8')), "\n";
try {
    mb_stripos('abc', 'a', 4, 'UTF-8');
} catch (ValueError $e) {
    echo get_class($e), "\n";
}
Output
at 0
missing
0
ValueError