01 · THE IDEAREAD → TRY → LEARN
Two questions about text
When you search inside a string, you usually want to know one of two things. Either you want to know whether some text appears, or you want to know where it first appears. PHP has a function for each question. str_contains() answers the yes-or-no question and returns a boolean. strpos() answers the location question and returns an integer position or false.
The signature of the location function is strpos(string $haystack, string $needle, int $offset = 0): int|false. The haystack is the text you search in, and the needle is the text you look for. The result is the zero-based position of the first match, counted from the start of the haystack.
The offset-zero trap
Because positions start at 0, a needle found at the very beginning of the haystack gives the integer 0. In PHP, 0 is falsy, and loose comparison treats 0 and false as equal. A test such as if (strpos($text, 'a')) therefore treats a match at the start as if nothing was found.
The first example shows this directly. The loose comparison $pos == false is true even though the needle was found, while the strict comparison $pos === false is false.
$text = 'abc';
$pos = strpos($text, 'a');
var_dump($pos);
var_dump($pos == false);
var_dump($pos === false);int(0) bool(true) bool(false)
The rule is simple. Always compare the result of strpos() with === or !==. Treat false as the only value that means "missing".
Choosing between the two functions
If you only need a yes-or-no answer, use str_contains($haystack, $needle). It returns true or false, so there is no zero-versus-false confusion to remember. Reach for strpos() when the position itself matters: for example, when you plan to cut the string at that point or search again after it.
The next example puts both functions side by side and also uses the third argument. With an offset, the search starts that many characters into the string, but the returned position is still measured from the beginning of the haystack.
$text = 'abcdef abcdef';
echo json_encode([
'first' => strpos($text, 'a'),
'afterOffset' => strpos($text, 'a', 1),
'missing' => strpos($text, 'z'),
'contains' => str_contains($text, 'def'),
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), "\n";{"first":0,"afterOffset":7,"missing":false,"contains":true}
The second a sits at position 7, and that is the number you get back, not 6 or 1.
Edge cases worth knowing
- Searching is case-sensitive.
strpos('Hello', 'h')finds nothing. A case-insensitive variant exists asstripos(). - An empty needle matches immediately. With the default offset,
strpos()returns0. - A negative offset starts the search that many characters from the end of the haystack.
- If the offset is greater than the length of the haystack, PHP throws a
ValueError.
The third example shows an empty needle, a negative offset and the error.
echo json_encode(strpos('abc', '')), "\n";
echo json_encode(strpos('abcabc', 'a', -3)), "\n";
try {
strpos('abc', 'a', 10);
} catch (ValueError $e) {
echo get_class($e), "\n";
}0 3 ValueError
In the negative-offset call, the search begins three characters from the end, so the first a it can see is at position 3.
Worked reasoning for the exercise
Your task is to wrap strpos() in a function that never returns false. Instead it returns the integer position, or -1 when the needle is missing. This is a common adapter when other code expects a plain integer.
Think through the steps in order:
- Call
strpos($haystack, $needle)and store the result in a variable. - Compare that variable with
falseusing===. - If it is identical to
false, return-1. Otherwise return the stored position unchanged.
Notice what a shortcut like strpos(...) ?: -1 would do. The short ternary replaces every falsy value, including a real position of 0, with -1. That is exactly the bug this lesson warns about. Your tests include a match at position 0 and an empty needle, which also produces 0, so they will expose it.
Exercise contract
Write positionOrMinusOne(string $haystack, string $needle): int. It returns the zero-based position of the first occurrence of $needle in $haystack, or -1 if there is none. Matching is case-sensitive. An empty needle returns 0. The tests check the returned integer, not printed output.
Recap
strpos()returns an integer position orfalse, and positions start at 0.- Use
===or!==to test the result. Never rely on truthiness. - Use
str_contains()when you only need a boolean. - Remember case sensitivity, the empty needle, negative offsets and the
ValueErrorfor an offset beyond the string length.
Functions · Arrays · Types
- Distinguish offset zero from a missing substring
- Implement and test a deterministic PHP 8.4 function