A PHP classic bugstrpos() Syndrom: When 0 And false Are Not The Same

In PHP, several functions can return 0 or false or even null, and all these values may be confused for different things.

The most popular function with this syndrom is strpos(), hence the name of this classic PHP bug. strpos() returns the position where the string was found. In the case of b below, it returns 1. And 1 is truthy.

When strpos() does not find the requested string, as it is the case for d, then it returns false.

<?php

if (strpos('abc', 'd')) {
  print "d is found!\n";
} else {
  print "d is not found!\n";
}

if (strpos('abc', 'b')) {
  print "b is found!\n";
} else {
  print "b is not found!\n";
}

if (strpos('abc', 'a')) {
  print "a is found!\n";
} else {
  print "a is not found!\n";
}

>

The problem arise when strpos() finds the requested string on the first position of the haystack. Then, the returned position is 0, which is falsy. At this point, string not found and string found at position 0 are confused, and it leads to a bug.

In this case, an error is converted to a false value, while a valid return value may also be confused with false, such as 0, '' (empty string), false, [] (empty array) or null. Nowadays, PHP tends to throw exceptions to avoid confusing one for the other, such as with json_decode(), though some previous behaviors are still in place.

There are some notable PHP functions which produce this behavior:

String Functions

  • strpos() – Returns the position (0 or higher) or false if not found.
  • stripos() – Case-insensitive version of strpos().
  • strrpos() – Returns the position of the last occurrence or false.
  • strripos() – Case-insensitive version of strrpos().
  • strstr() – Returns the substring or false if not found.
  • stristr() – Case-insensitive version of strstr().
  • strpbrk() – Returns the substring or false if not found.

Array Functions

  • array_search() – Returns the key (which can be 0) or false if not found.

Other Functions

  • preg_match() – Returns 1 for a match, 0 for no match, or false on error.
  • preg_match_all() – Returns the number of matches (which can be 0) or false on error.
  • json_decode() – May return valid decoded false, 0 or null values.

Best Practices

Strict comparison

One solution to protect one’s code against this problem is to always use a strict comparison, with the === or !== operators. )** when checking the return value of these functions to avoid confusion between 0 and false.

Example: “`

 <?php
<div><pre><code class="language-none">if (strpos($haystack, $needle) !== false) {
  // Found
}</code></pre></div>
> 

“`

Use safe functions

Another solution is to use dedicated functions, such as str_contains(), str_start_with()or str_end_with(), instead of strpos().

There isn’t always a straightforward replacement for the orginal function, such as for json_decode(), or preg_match(). In that case, you should create it yourself, with the added context.

Example: “`

 <?php
<div><pre><code class="language-none">// replace strpos() with str_contains()
if (str_contains($haystack, $needle)) {
// Found
}

function my_json_decode(string $json): string {
    if ($json === 'null') {
      throw new Exception("JSON cannot only hold NULL");
    }

    $return = json_decode($json) ?: '';

    return $return;
}</code></pre></div>
> 

“`

Use static analysis

Static analysis tools, like Exakat, are able to detect this error and focus attention on them before the code goes to production.

Explicit comparison

The strpos() syndrom is not limited to that function: it comes also with array_search(), and others. It usually takes a few hours of head scratching to find the error and make the code work again.

This is a PHP classic bug that should be known to every developer that work with the platform.