Strings7 min readExamples run on PHP 8.5.10

strpos()

Learn how PHP 8.4 strpos() finds the first position of a substring, how offsets work, and why you must compare the result with === false.

Signature

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

Purpose

strpos() looks for the first occurrence of one string (the needle) inside another string (the haystack). If it finds the needle, it returns the zero-based position where the match starts. If it does not, it returns false. It is the standard tool when you need to know where something is, for example to split a string at a separator or to scan for repeated matches.

If you only need a yes/no answer, str_contains() is usually clearer. If you need to ignore case, use stripos().

Signature

Parameters and defaults

  • $haystack: the string to search in.
  • $needle: the string to look for. The search is case-sensitive. Since PHP 8.0 an empty string is accepted, and passing an integer as the needle is no longer supported, so cast or convert explicitly if you hold a non-string value.
  • $offset: where the search begins. The default is 0, the start of the haystack. A positive value skips that many bytes from the start. A negative value starts that many bytes back from the end of the haystack (supported since PHP 7.1).

Return value and failure cases

The function returns an int position counted from the beginning of the haystack, regardless of the offset you passed. Positions start at 0, not 1. When the needle is not found, the result is false.

If $offset is greater than the length of the haystack, PHP throws a ValueError. An offset exactly equal to the length is allowed; it simply leaves nothing to search.

Because a match at the very start returns 0, which is loosely equal to false, you must test the result with === or !==.

Example 1: strict comparison

The first call matches at position 0. A loose comparison confuses that with failure; a strict one does not.

Example
<?php
$haystack = 'abc';
$needle = 'a';
$pos = strpos($haystack, $needle);
var_dump($pos);
var_dump(strpos($haystack, 'z'));
var_dump($pos == false);
var_dump($pos === false);
Output
int(0)
bool(false)
bool(true)
bool(false)

Example 2: positive and negative offsets

In 'abcdef abcdef' the letter a sits at positions 0 and 7. The returned number is always relative to the start of the whole string, even when the search starts later.

Example
<?php
$s = 'abcdef abcdef';
$results = [
    'from start' => strpos($s, 'a'),
    'offset 1' => strpos($s, 'a', 1),
    'offset -6' => strpos($s, 'a', -6),
    'f from -3' => strpos($s, 'f', -3),
    'a from -1' => strpos($s, 'a', -1),
];
echo json_encode($results), PHP_EOL;
Output
{"from start":0,"offset 1":7,"offset -6":7,"f from -3":12,"a from -1":false}

The offset -6 starts at position 7 in this 13-byte string. The last lookup starts at the final character, f, so no a is found.

Example 3: finding every occurrence

After each match, move the offset past the match and search again. This helper assumes a non-empty needle.

Example
<?php
function findAll(string $haystack, string $needle): array
{
    $positions = [];
    $offset = 0;
    while (($pos = strpos($haystack, $needle, $offset)) !== false) {
        $positions[] = $pos;
        $offset = $pos + strlen($needle);
    }
    return $positions;
}

echo json_encode(findAll('banana', 'an')), PHP_EOL;
echo json_encode(findAll('aaaa', 'aa')), PHP_EOL;
Output
[1,3]
[0,2]

The second call shows non-overlapping matches: positions 0 and 2, not 0, 1 and 2. Advance the offset by 1 instead if you want overlapping matches.

Edge cases

  • Empty needle: it matches at every position, so strpos() returns the offset you supplied, or 0 by default. Never use an empty needle in a loop like the one above, because the offset would never advance.
  • Offset beyond the end: an offset larger than the haystack length throws ValueError. Check strlen() first, or catch the exception, when the offset comes from user input.
  • Bytes, not characters: the function is binary-safe and counts bytes. In UTF-8 text with multibyte characters, the position is a byte index. Use mb_strpos() when you need a character index.
  • Case sensitivity: strpos('Hello', 'h') is false. Use stripos() for case-insensitive search.

Common mistakes

  1. Using if (strpos(...)) or == false. A match at position 0 is treated as a failure. Always write === false or !== false.
  2. Assuming the result is relative to the offset. The result is measured from the start of the haystack, so do not add the offset to it again.
  3. Swapping the arguments. The haystack comes first and the needle second; reversing them silently searches for the wrong thing.
  4. Expecting character positions in multibyte text. Byte positions can differ from visible character counts.
  5. Using strpos() for a simple containment check. str_contains() expresses the intent better and avoids the 0 versus false trap.
  • stripos(): case-insensitive first occurrence.
  • strrpos() and strripos(): last occurrence.
  • str_contains(), str_starts_with(), str_ends_with(): boolean checks.
  • strstr(): returns the part of the string from the first match.
  • strpbrk(): searches for any of a set of characters.
  • substr(): extracts text once you know a position.
  • preg_match(): pattern-based matching.

Recap

strpos() returns the zero-based byte position of the first case-sensitive match, or false when there is none. Compare with ===, remember that the result is independent of the offset, and keep the offset within the haystack length to avoid a ValueError.

Tested examples

Strict comparison with a match at position 0
<?php
$haystack = 'abc';
$needle = 'a';
$pos = strpos($haystack, $needle);
var_dump($pos);
var_dump(strpos($haystack, 'z'));
var_dump($pos == false);
var_dump($pos === false);
Output
int(0)
bool(false)
bool(true)
bool(false)
Positive and negative offsets
<?php
$s = 'abcdef abcdef';
$results = [
    'from start' => strpos($s, 'a'),
    'offset 1' => strpos($s, 'a', 1),
    'offset -6' => strpos($s, 'a', -6),
    'f from -3' => strpos($s, 'f', -3),
    'a from -1' => strpos($s, 'a', -1),
];
echo json_encode($results), PHP_EOL;
Output
{"from start":0,"offset 1":7,"offset -6":7,"f from -3":12,"a from -1":false}
Finding every occurrence with a moving offset
<?php
function findAll(string $haystack, string $needle): array
{
    $positions = [];
    $offset = 0;
    while (($pos = strpos($haystack, $needle, $offset)) !== false) {
        $positions[] = $pos;
        $offset = $pos + strlen($needle);
    }
    return $positions;
}

echo json_encode(findAll('banana', 'an')), PHP_EOL;
echo json_encode(findAll('aaaa', 'aa')), PHP_EOL;
Output
[1,3]
[0,2]