Strings7 min readExamples run on PHP 8.5.10

str_contains()

Learn how PHP 8.4 str_contains() performs a case-sensitive substring check, how it treats empty strings, and which mistakes to avoid.

Signature

str_contains(string $haystack, string $needle): bool

Purpose

str_contains() answers one question: does a string contain a given piece of text anywhere inside it? It returns a plain boolean, so it fits directly into if conditions, filters and validation rules. It was added in PHP 8 and replaces the older habit of writing strpos($haystack, $needle) !== false.

The check is case-sensitive and binary-safe. That means 'Lazy' does not match 'lazy', and the strings may contain any bytes, including null bytes.

Signature

Parameters and defaults

  • $haystack: the string to search in.
  • $needle: the substring to look for.

Neither parameter has a default value, so both are required. The function only tells you whether the needle occurs, not where. If you need the position, use strpos() instead.

Return value and failure cases

The function returns true when $needle occurs in $haystack and false otherwise. It does not return an integer or null, so the false versus 0 trap of strpos() does not exist here.

The function has no error return of its own. The only failures come from PHP's ordinary argument typing. Under declare(strict_types=1), passing something that is not a string throws a TypeError. Without strict types, scalar values such as integers are converted to strings first.

Example 1: case-sensitive matching

The first example checks three needles against one sentence. Only the exact casing matches.

Example
<?php
$text = 'The lazy fox jumped over the fence';
echo json_encode([
    'lazy' => str_contains($text, 'lazy'),
    'Lazy' => str_contains($text, 'Lazy'),
    'fox' => str_contains($text, 'fox'),
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
echo "\n";
Output
{"lazy":true,"Lazy":false,"fox":true}

Example 2: empty strings and long needles

An empty needle is considered to be contained in every string, even in an empty haystack. A needle longer than the haystack can never match.

Example
<?php
echo json_encode([
    'abc_empty' => str_contains('abc', ''),
    'empty_empty' => str_contains('', ''),
    'empty_a' => str_contains('', 'a'),
    'longer_needle' => str_contains('ab', 'abc'),
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
echo "\n";
Output
{"abc_empty":true,"empty_empty":true,"empty_a":false,"longer_needle":false}

Example 3: a small keyword filter

Because matching is case-sensitive, a case-insensitive search needs both strings normalized first. This helper lowercases ASCII text with strtolower() and skips empty needles. Without that guard, an empty keyword would match every message.

Example
<?php
function containsAnyIgnoreCase(string $text, array $needles): bool
{
    $lower = strtolower($text);
    foreach ($needles as $needle) {
        if ($needle !== '' && str_contains($lower, strtolower($needle))) {
            return true;
        }
    }
    return false;
}

$messages = ['Order #1001 shipped', 'URGENT: password reset', 'Weekly newsletter'];
$flagged = array_values(array_filter(
    $messages,
    fn(string $message): bool => containsAnyIgnoreCase($message, ['urgent', 'password'])
));
echo json_encode($flagged, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
echo "\n";
Output
["URGENT: password reset"]

Edge cases

  • Empty needle: always true. If user input could be empty, check it explicitly before calling the function.
  • Empty haystack: only an empty needle can match.
  • Whitespace counts: 'a b' contains ' ', but 'ab' does not. The function never trims anything.
  • Binary data: the search works on bytes, so null bytes are treated like any other character.
  • Multibyte text: the comparison is byte-based. Searching for a complete UTF-8 character normally works as expected. The function does not understand case rules or normalization forms, so visually identical text in different Unicode forms may not match.

Common mistakes

  1. Expecting case-insensitive matching. str_contains('Hello', 'hello') is false. Normalize both sides first, or use stripos() when you only need a yes or no answer.
  2. Forgetting the empty needle. Code like str_contains($text, $keyword) with an empty $keyword accepts every input.
  3. Using it for position-dependent checks. For prefix and suffix tests, str_starts_with() and str_ends_with() state the intent more precisely. A match in the middle of the string is not the same as a match at the start.
  4. Using it for pattern matching. The needle is literal text. Characters such as . or * have no special meaning. Use preg_match() when you need a pattern.
  5. Mixing up argument order. The haystack comes first and the needle second. Swapping them still runs, but it asks the opposite question.
  6. Using it as a security filter. A substring check is easy to bypass with different casing, encodings or spacing. Validate with an allow-list where you can.
  • str_starts_with() checks the beginning of a string.
  • str_ends_with() checks the end of a string.
  • stripos() finds a case-insensitive position.
  • strpos() and strrpos() return the first or last position of a match.
  • strstr() returns the part of the string starting at the first match.
  • strpbrk() searches for any of a set of characters.
  • substr() extracts part of a string.
  • preg_match() performs regular expression matching.

Recap

str_contains() is the clearest way to ask whether one string appears inside another. It returns a boolean, is case-sensitive, works on bytes, and treats the empty needle as always present. Normalize case yourself when needed, guard against empty needles, and choose a more specific function when position or pattern matters.

Tested examples

Case-sensitive matching
<?php
$text = 'The lazy fox jumped over the fence';
echo json_encode([
    'lazy' => str_contains($text, 'lazy'),
    'Lazy' => str_contains($text, 'Lazy'),
    'fox' => str_contains($text, 'fox'),
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
echo "\n";
Output
{"lazy":true,"Lazy":false,"fox":true}
Empty strings and long needles
<?php
echo json_encode([
    'abc_empty' => str_contains('abc', ''),
    'empty_empty' => str_contains('', ''),
    'empty_a' => str_contains('', 'a'),
    'longer_needle' => str_contains('ab', 'abc'),
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
echo "\n";
Output
{"abc_empty":true,"empty_empty":true,"empty_a":false,"longer_needle":false}
A small keyword filter
<?php
function containsAnyIgnoreCase(string $text, array $needles): bool
{
    $lower = strtolower($text);
    foreach ($needles as $needle) {
        if ($needle !== '' && str_contains($lower, strtolower($needle))) {
            return true;
        }
    }
    return false;
}

$messages = ['Order #1001 shipped', 'URGENT: password reset', 'Weekly newsletter'];
$flagged = array_values(array_filter(
    $messages,
    fn(string $message): bool => containsAnyIgnoreCase($message, ['urgent', 'password'])
));
echo json_encode($flagged, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
echo "\n";
Output
["URGENT: password reset"]