Lesson 32 of 100
Strings15 min5 checksPHP 8.5

Find Text with strpos and str_contains

Learn how strpos() reports positions, why offset zero is not the same as a missing match, and when str_contains() is the clearer choice.

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.

Example
<?php
$text = 'abc';
$pos = strpos($text, 'a');
var_dump($pos);
var_dump($pos == false);
var_dump($pos === false);
Output
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.

Example
<?php
$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";
Output
{"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 as stripos().
  • An empty needle matches immediately. With the default offset, strpos() returns 0.
  • 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.

Example
<?php
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";
}
Output
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:

  1. Call strpos($haystack, $needle) and store the result in a variable.
  2. Compare that variable with false using ===.
  3. 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 or false, 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 ValueError for an offset beyond the string length.
Before you start

Functions · Arrays · Types

You’ll practice
  • Distinguish offset zero from a missing substring
  • Implement and test a deterministic PHP 8.4 function
Your task

Make positionOrMinusOne() pass all 5 checks. Run your code as often as you like, then check your answer.

exercise.phpSaved in this browserPHP 8.5
CONSOLE & CHECKS● Ready

Ready when you are

Your output will appear here.
Make it work. Then make it pass.

PHP loads on your first run. Each check starts fresh. Your code stays in this browser; clearing site data removes your progress. A check or the task looks wrong? Report a problem with this exercise.