Strings7 min readExamples run on PHP 8.5.10

stripos()

Learn how PHP 8.4 stripos() finds the first case-insensitive match in a string, how offsets work, and why you must compare the result with ===.

Signature

stripos(string $haystack, string $needle, int $offset = 0): int|false

Purpose

stripos() answers the question "where does this text first appear, ignoring upper and lower case?" It scans a string called the haystack for another string called the needle and returns the zero-based position of the first match. It is the case-insensitive sibling of strpos(). Typical uses include checking whether a message mentions a keyword, locating a marker such as ERROR in a log line, or finding where to cut a string.

Signature

Parameters and defaults

  • $haystack: the string to search in.
  • $needle: the string to look for. Since PHP 8.0 an empty string is accepted, and an integer is no longer treated as a character code. If you hold a number, cast it to a string yourself.
  • $offset: where the search begins. The default is 0, the start of the string. A positive value skips that many characters from the start. A negative value starts that many characters back from the end of the haystack.

Return value and failure cases

On success you get an int: the position of the match counted from the beginning of the haystack. The position is not relative to $offset, so searching from offset 4 and finding a match at index 6 returns 6. Positions start at 0, not 1.

If the needle does not occur, the function returns false. If $offset is larger than the length of the haystack, PHP throws a ValueError.

Because a match at the very start is 0 and 0 is falsy, you must test the result with === or !==. A plain if (stripos(...)) treats a match at the start as a failure.

The search ignores case in both the haystack and the needle. A missing needle produces false, which json_encode() prints as false.

Example
<?php
$text = 'PHP Makes Web Pages';
$result = [
    'lower' => stripos($text, 'makes'),
    'upper' => stripos($text, 'WEB'),
    'missing' => stripos($text, 'python'),
];
echo json_encode($result, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), PHP_EOL;
Output
{"lower":4,"upper":10,"missing":false}

Example 2: testing the result safely

A helper that checks whether a string begins with a word shows why strict comparison matters. The position 0 means "found at the start", and only === 0 expresses that exactly.

Example
<?php
function startsWithWord(string $text, string $word): bool
{
    return stripos($text, $word) === 0;
}

$checks = [
    startsWithWord('Hello world', 'hello'),
    startsWithWord('Say hello', 'hello'),
    startsWithWord('Nothing', 'hello'),
];
echo json_encode($checks, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), PHP_EOL;
Output
[true,false,false]

For a simple prefix test, str_starts_with() is case-sensitive, so stripos($text, $word) === 0 is useful when case should be ignored. Be aware that it still scans the whole string when there is no match at the start.

Example 3: offsets, empty needles and errors

The string abcABCabc contains three copies of the letters, at positions 0, 3 and 6. The example shows positive offsets, negative offsets, an empty needle and an out-of-range offset.

Example
<?php
$text = 'abcABCabc';
$result = [
    'from_1' => stripos($text, 'abc', 1),
    'from_4' => stripos($text, 'ABC', 4),
    'negative_3' => stripos($text, 'abc', -3),
    'negative_2' => stripos($text, 'abc', -2),
    'empty_needle' => stripos($text, '', 3),
];

try {
    stripos($text, 'a', 20);
} catch (ValueError $e) {
    $result['error'] = get_class($e);
}

echo json_encode($result, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), PHP_EOL;
Output
{"from_1":3,"from_4":6,"negative_3":6,"negative_2":false,"empty_needle":3,"error":"ValueError"}

With an offset of -3 the search starts at index 6, so the last copy matches. With -2 it starts at index 7, where only bc remains, so there is no match. An empty needle matches immediately at the offset, so it returns the offset itself.

Edge cases

  • ASCII-only case folding. As of PHP 8.2 the comparison no longer depends on the locale set with setlocale(). Only ASCII letters are folded; other bytes are compared by byte value. For accented or other multibyte text, use mb_stripos().
  • Byte positions. The returned index counts bytes, not visible characters. In UTF-8 text with multibyte characters before the match, the number may be larger than the character count.
  • Binary safety. The function is binary-safe, so strings containing null bytes are searched correctly.
  • Offset equal to length. An offset equal to the haystack length is allowed. Only a larger offset throws.

Common mistakes

  1. Writing if (stripos($s, $n)) or != false. A match at position 0 is then mistaken for no match. Use !== false.
  2. Assuming the returned position is relative to $offset. It is always relative to the start of the haystack.
  3. Passing an integer as the needle and expecting a character code. This has not worked since PHP 8.0; use chr() or a string.
  4. Using stripos() when you only need a yes/no answer. str_contains() is case-sensitive, so for a case-insensitive boolean check stripos($h, $n) !== false is still the right idiom.
  5. Using it for non-ASCII case-insensitive searches and expecting É to match é.
  • strpos(): the case-sensitive version.
  • strripos() and strrpos(): find the last occurrence.
  • mb_stripos(): multibyte-aware case-insensitive search.
  • stristr(): returns the part of the string from the match onward.
  • str_contains(), str_starts_with(), str_ends_with(): case-sensitive boolean checks.
  • substr() and str_ireplace(): commonly combined with a found position.

Recap

stripos() returns the zero-based byte position of the first case-insensitive match, or false when nothing matches. Always compare with === or !==, remember that the position is measured from the start of the haystack, and expect a ValueError for an offset beyond the string length. Use the mb_ variant for multibyte text.

Tested examples

Basic case-insensitive search
<?php
$text = 'PHP Makes Web Pages';
$result = [
    'lower' => stripos($text, 'makes'),
    'upper' => stripos($text, 'WEB'),
    'missing' => stripos($text, 'python'),
];
echo json_encode($result, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), PHP_EOL;
Output
{"lower":4,"upper":10,"missing":false}
Testing the result with strict comparison
<?php
function startsWithWord(string $text, string $word): bool
{
    return stripos($text, $word) === 0;
}

$checks = [
    startsWithWord('Hello world', 'hello'),
    startsWithWord('Say hello', 'hello'),
    startsWithWord('Nothing', 'hello'),
];
echo json_encode($checks, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), PHP_EOL;
Output
[true,false,false]
Offsets, empty needle and ValueError
<?php
$text = 'abcABCabc';
$result = [
    'from_1' => stripos($text, 'abc', 1),
    'from_4' => stripos($text, 'ABC', 4),
    'negative_3' => stripos($text, 'abc', -3),
    'negative_2' => stripos($text, 'abc', -2),
    'empty_needle' => stripos($text, '', 3),
];

try {
    stripos($text, 'a', 20);
} catch (ValueError $e) {
    $result['error'] = get_class($e);
}

echo json_encode($result, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), PHP_EOL;
Output
{"from_1":3,"from_4":6,"negative_3":6,"negative_2":false,"empty_needle":3,"error":"ValueError"}