Signature
str_contains(string $haystack, string $needle): boolPurpose
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.
$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";{"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.
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";{"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.
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";["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
- Expecting case-insensitive matching.
str_contains('Hello', 'hello')isfalse. Normalize both sides first, or usestripos()when you only need a yes or no answer. - Forgetting the empty needle. Code like
str_contains($text, $keyword)with an empty$keywordaccepts every input. - Using it for position-dependent checks. For prefix and suffix tests,
str_starts_with()andstr_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. - Using it for pattern matching. The needle is literal text. Characters such as
.or*have no special meaning. Usepreg_match()when you need a pattern. - Mixing up argument order. The haystack comes first and the needle second. Swapping them still runs, but it asks the opposite question.
- 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.
Related functions
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()andstrrpos()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
$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";
{"lazy":true,"Lazy":false,"fox":true}
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";
{"abc_empty":true,"empty_empty":true,"empty_a":false,"longer_needle":false}
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";
["URGENT: password reset"]