Search in Strings

Ma Mariya Vinciya Updated 17 Oct 2026
11 min read ·Lesson 50 of 102
In this post

Searching inside strings is one of the most common operations in PHP: checking whether an email belongs to a domain, whether a URL uses HTTPS, whether a comment contains a banned word, or where a deli

Searching inside strings is one of the most common operations in PHP: checking whether an email belongs to a domain, whether a URL uses HTTPS, whether a comment contains a banned word, or where a delimiter appears so the text can be split.

PHP offers several groups of search functions:

QuestionFunctionsReturn
Does the text contain / start with / end with something?str_contains(), str_starts_with(), str_ends_with()true / false
Where is it?strpos(), stripos(), strrpos(), strripos() and mb_* versionsPosition (int) or false
What follows it?strstr(), stristr(), strrchr()Part of the string or false
How many times does it appear?substr_count()int
Does it match a pattern?preg_match()1, 0, or false

str_contains() — Does the Text Contain Something?

Syntax (PHP 8.0+)

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

The haystack is the text to search in; the needle is the text to look for.

Example

<?php
$sentence = "The quick brown fox jumps over the lazy dog";

var_dump(str_contains($sentence, "brown"));
var_dump(str_contains($sentence, "Brown"));
var_dump(str_contains($sentence, "cat"));

Output

bool(true)
bool(false)
bool(false)

How It Works

  • str_contains() returns a real boolean, so it can be used directly in if statements.
  • The search is case-sensitive: Brown does not match brown.

Case-Insensitive Check

There is no str_icontains(). Convert both strings to the same case first:

<?php
$sentence = "The quick brown fox";
$search = "BROWN";

var_dump(str_contains(mb_strtolower($sentence), mb_strtolower($search)));

Output

bool(true)

mb_strtolower() handles non-English letters correctly. Alternatively, stripos($sentence, $search) !== false performs a case-insensitive search for ASCII letters.

Why str_contains() Was Added

Before PHP 8.0, the usual check was strpos($haystack, $needle) !== false. Writing if (strpos(...)) instead was a very common bug, because a match at position 0 is falsy (see the strpos() section). str_contains() expresses the intent directly and returns a boolean, removing that trap.


str_starts_with() and str_ends_with()

Syntax (PHP 8.0+)

str_starts_with(string $haystack, string $needle): bool
str_ends_with(string $haystack, string $needle): bool

Example

<?php
$url = "https://gocourse.in/php/strings";
$file = "invoice-1042.PDF";
$phone = "+91 98765 43210";

var_dump(str_starts_with($url, "https://"));
var_dump(str_ends_with($file, ".pdf"));
var_dump(str_ends_with(strtolower($file), ".pdf"));
var_dump(str_starts_with($phone, "+91"));

Output

bool(true)
bool(false)
bool(true)
bool(true)

How It Works

  • str_starts_with() checks only the beginning of the string; str_ends_with() checks only the end.
  • .PDF does not match .pdf because both functions are case-sensitive. Lowercasing the file name first makes the check reliable.

Practical Uses

CheckExample
Is a URL secure?str_starts_with($url, 'https://')
Is this a local link?str_starts_with($href, '/')
Does a route belong to the admin area?str_starts_with($path, '/admin/')
Is this an image file name?str_ends_with(strtolower($name), '.png')
Does a value have a prefix to strip?str_starts_with($code, 'SKU-')

Security note: Checking the file name extension with str_ends_with() is fine for organizing files, but it does not prove a file is safe — a malicious upload can be named virus.png. File upload validation is covered in File Upload Security.


The Empty Needle Trap

All three boolean functions return true when the needle is an empty string:

<?php
var_dump(str_contains("anything", ""));
var_dump(str_starts_with("anything", ""));
var_dump(str_ends_with("anything", ""));

Output

bool(true)
bool(true)
bool(true)

This is mathematically consistent (every string contains the empty string), but it causes bugs when the needle comes from user input:

<?php
$products = ["Keyboard", "Mouse", "Monitor"];
$query = trim($_GET['q'] ?? '');

$matches = array_filter($products, fn($p) => str_contains($p, $query));

If the search box is empty, every product matches. Decide explicitly what an empty search should do:

<?php
if ($query === '') {
    $matches = [];  // or show all products intentionally
}

strpos() — Find the Position

Syntax

strpos(string $haystack, string $needle, int $offset = 0): int|false

strpos() returns the position (0-based) of the first occurrence of the needle, or false if it is not found.

Example

<?php
$text = "Hello World";

var_dump(strpos($text, "o"));
var_dump(strpos($text, "World"));
var_dump(strpos($text, "H"));
var_dump(strpos($text, "z"));

Output

int(4)
int(6)
int(0)
bool(false)

The Position-Zero Problem

strpos($text, "H") returns 0, because H is at the very start. In a loose condition, 0 is falsy:

<?php
if (strpos("Hello World", "H")) {
    echo "Found";
} else {
    echo "Not found";
}

Output

Not found

The text was found, but if (0) behaves like if (false). Always compare the result strictly:

<?php
if (strpos("Hello World", "H") !== false) {
    echo "Found";
}

Output

Found

Rule: use str_contains() when you only need yes/no. Use strpos() when you need the position, and always check it with === false or !== false.

Using the Offset

The third argument tells strpos() where to start searching.

<?php
$text = "Hello World";

var_dump(strpos($text, "o"));        // first "o"
var_dump(strpos($text, "o", 5));     // first "o" at or after position 5
var_dump(strpos("banana", "a", -3)); // start 3 characters from the end

Output

int(4)
int(7)
int(3)

A negative offset starts the search that many characters from the end. In "banana", offset -3 starts at position 3 ("ana"), where the first a is found.

Finding All Occurrences

The offset makes it possible to find every occurrence in a loop:

<?php
function allPositions(string $haystack, string $needle): array
{
    if ($needle === '') {
        return [];
    }

    $positions = [];
    $offset = 0;

    while (($pos = strpos($haystack, $needle, $offset)) !== false) {
        $positions[] = $pos;
        $offset = $pos + strlen($needle);
    }

    return $positions;
}

print_r(allPositions("banana", "an"));
print_r(allPositions("PHP is fun. PHP is fast.", "PHP"));

Output

Array
(
    [0] => 1
    [1] => 3
)
Array
(
    [0] => 0
    [1] => 12
)

How It Works

  1. ($pos = strpos(...)) !== false assigns the result to $pos and checks it in one expression. The parentheses are required so that the assignment happens before the comparison.
  2. Each found position is stored.
  3. The next search starts after the current match ($pos + strlen($needle)), which prevents finding the same occurrence again.
  4. The loop ends when strpos() returns false.
  5. The empty-needle guard prevents an endless loop.

Other Position Functions

FunctionFindsCase
strpos()First occurrenceSensitive
stripos()First occurrenceInsensitive (ASCII)
strrpos()Last occurrenceSensitive
strripos()Last occurrenceInsensitive (ASCII)

Example

<?php
$path = "/var/www/html/uploads/report.final.pdf";

var_dump(stripos("Hello World", "WORLD"));
var_dump(strrpos($path, "/"));
var_dump(strrpos($path, "."));

$fileName = substr($path, strrpos($path, "/") + 1);
var_dump($fileName);

Output

int(6)
int(21)
int(34)
string(16) "report.final.pdf"

How It Works

  • stripos() ignores case, so WORLD matches World at position 6.
  • strrpos($path, "/") finds the last slash (position 21), which separates the folder from the file name.
  • strrpos($path, ".") finds the last dot (position 34), which starts the file extension — not the first dot in report.final.
  • substr($path, $lastSlash + 1) takes everything after the last slash.

For file paths, basename($path) and pathinfo($path, PATHINFO_EXTENSION) return these parts directly and are clearer. Position functions are most useful when parsing custom formats.


Multibyte Search: mb_strpos() and Friends

Byte-based functions return byte positions. For UTF-8 text, those differ from character positions:

<?php
$text = "₹500 only";

var_dump(strpos($text, "only"));
var_dump(mb_strpos($text, "only"));

Output

int(7)
int(5)

₹ occupies 3 bytes, so strpos() reports byte 7, while mb_strpos() reports character 5. Mixing the two leads to broken slices: a byte position passed to mb_substr() (or a character position passed to substr()) cuts in the wrong place.

Rule: if you search with mb_strpos(), slice with mb_substr(). If you search with strpos(), slice with substr().

Multibyte equivalents: mb_strpos(), mb_stripos(), mb_strrpos(), mb_strripos(), mb_strstr(), mb_stristr(), mb_substr_count(). mb_stripos() also performs case-insensitive matching for non-English letters.

Note: str_contains(), str_starts_with(), and str_ends_with() work correctly with UTF-8 text, because they compare complete byte sequences and return only true or false.


strstr() and stristr() — Get What Follows

strstr() returns the part of the haystack starting at the first occurrence of the needle (or the part before it with true as the third argument). stristr() is the case-insensitive version.

<?php
$email = "Priya.K@GoCourse.in";

echo strstr($email, "@") . "\n";
echo strstr($email, "@", true) . "\n";
echo stristr($email, "gocourse") . "\n";
var_dump(strstr($email, "#"));

Output

@GoCourse.in
Priya.K
GoCourse.in
bool(false)

stristr() finds gocourse regardless of case and returns the text with its original capitalization. When you only need to know whether text exists, use str_contains() — it is clearer than strstr(). Slicing with these functions is covered in Slicing Strings.


substr_count() — Count Occurrences

<?php
$log = "ERROR db; WARN cache; ERROR api; INFO ok; ERROR mail";

echo substr_count($log, "ERROR") . " errors\n";
echo substr_count($log, "WARN") . " warnings\n";
echo substr_count("aaaa", "aa") . "\n";

Output

3 errors
1 warnings
2

substr_count() counts non-overlapping occurrences: "aaaa" contains "aa" twice (positions 0 and 2), not three times. It is case-sensitive; lowercase both strings for case-insensitive counting.


strpbrk() — Find Any of Several Characters

<?php
var_dump(strpbrk("user@example.com", "@."));
var_dump(strpbrk("hello world", "0123456789"));

Output

string(12) "@example.com"
bool(false)

strpbrk() searches for the first occurrence of any character from the list and returns the rest of the string from there. It is useful for quick checks such as "does this input contain any digits?" (it does not, so the second call returns false).


Whole-Word and Pattern Searches: preg_match()

String functions match text anywhere, including inside other words:

<?php
$text = "The category page shows a cat photo.";

var_dump(substr_count($text, "cat"));
var_dump(preg_match_all('/\bcat\b/i', $text));

Output

int(2)
int(1)
  • substr_count() finds cat twice: once inside category and once as a word.
  • \b in a regular expression marks a word boundary, so /\bcat\b/i matches only the standalone word. The i modifier makes it case-insensitive. preg_match_all() returns the number of matches.

Use regular expressions when you need whole words, alternatives (cat|dog), or patterns (digits, email formats). For plain substring checks, the string functions are simpler and faster. See preg_match.


Checking Whether a Value Is One of a List

Searching a string is different from checking whether a value equals one of several allowed values. For the second task, use an array:

<?php
$allowedSorts = ['price', 'name', 'newest'];
$sort = $_GET['sort'] ?? 'newest';

if (!in_array($sort, $allowedSorts, true)) {
    $sort = 'newest';
}

A common mistake is str_contains('price,name,newest', $sort), which also accepts partial values like "ice" or "name,new". in_array() with the third argument true checks for an exact, type-strict match.


Practical Example: Search and Highlight

<?php
function searchProducts(array $products, string $query): array
{
    $query = mb_strtolower(trim($query));

    if ($query === '') {
        return [];
    }

    return array_values(array_filter(
        $products,
        fn(string $name) => str_contains(mb_strtolower($name), $query)
    ));
}

function highlight(string $text, string $term): string
{
    $safeText = htmlspecialchars($text);
    $safeTerm = htmlspecialchars(trim($term));

    if ($safeTerm === '') {
        return $safeText;
    }

    $pattern = '/' . preg_quote($safeTerm, '/') . '/iu';

    return preg_replace($pattern, '<mark>$0</mark>', $safeText);
}

$products = ['Wireless Mouse', 'USB Keyboard', 'Mouse Pad', 'Monitor Stand'];

foreach (searchProducts($products, ' MOUSE ') as $name) {
    echo highlight($name, 'mouse') . "\n";
}

echo highlight('Learn PHP & MySQL with php examples', 'php') . "\n";

Output

Wireless <mark>Mouse</mark>
<mark>Mouse</mark> Pad
Learn <mark>PHP</mark> & MySQL with <mark>php</mark> examples

Step-by-Step Explanation

searchProducts()

  1. The query is trimmed and lowercased with mb_strtolower(), so ' MOUSE ' becomes 'mouse'.
  2. An empty query returns no results instead of matching everything.
  3. array_filter() keeps products whose lowercased name contains the query; array_values() re-numbers the results from 0.

highlight()

  1. The text and the term are escaped with htmlspecialchars() before highlighting, so the output is safe HTML (notice & becomes &).
  2. preg_quote() escapes characters in the term that have special meaning in regular expressions, such as . or +, so user input cannot change the pattern.
  3. The i modifier makes the match case-insensitive; u makes the pattern work with UTF-8 text.
  4. $0 in the replacement inserts the matched text with its original capitalization, wrapped in <mark>.

This is the basis of the search-and-highlight feature found on many websites. For large data sets, searching is done in the database (for example with SQL LIKE or full-text indexes) rather than in PHP arrays.


Common Mistakes

Checking strpos() Loosely

Effect: A match at position 0 is treated as "not found".

Fix: !== false, or str_contains().

Forgetting Case Sensitivity

Effect: str_contains("Hello", "hello") is false.

Fix: Lowercase both values, or use stripos() / mb_stripos().

Searching With an Empty Needle

Effect: Every string matches.

Fix: Handle empty search terms explicitly.

Mixing Byte and Character Positions

Effect: Slices cut through multibyte characters.

Fix: Pair strpos() with substr(), and mb_strpos() with mb_substr().

Using Substring Search for Whole Words or Exact Values

Effect: cat matches category; "ice" matches "price,name".

Fix: Use preg_match('/\bword\b/') for words and in_array($v, $list, true) for exact values.

Highlighting Without Escaping

Effect: User input in search results enables XSS.

Fix: Escape text and term first, then add markup.


Best Practices

  • Use str_contains(), str_starts_with(), and str_ends_with() for yes/no checks (PHP 8.0+).
  • Use strpos() family functions only when you need positions, and compare with !== false.
  • Normalize case (with mb_strtolower()) for case-insensitive searches of user-facing text.
  • Use the mb_* position functions with mb_substr() for UTF-8 text.
  • Use regular expressions for word boundaries and patterns, and preg_quote() for user-supplied terms.
  • Validate allowed values with in_array(..., true), not substring search.

Official reference: https://www.php.net/manual/en/function.str-contains.php

0 Comments

Reviewed before they appear

No comments yet.

PHP
PHP Home PHP Switch PHP Match Alternative Syntax PHP Loops While Loop Do While Loop For Loop Foreach Loop Nested Loops Break Statement PHP Home PHP Intro PHP Install PHP Built-in Web Server PHP Syntax PHP Tags PHP and HTML PHP Comments PHP Multiline Comments PHP Variables Variables Scope PHP Casting Logical Operators Null Coalescing Operator Spaceship Operator Operator Precedence Continue Statement Modify Strings PHP If...Else...Elseif PHP If Modify Strings PHP If Operators PHP Functions Concatenate Strings PHP If...Else Slicing Strings User-defined Functions Slicing Strings PHP Shorthand if Escape Characters Default Argument Values PHP Nested if Pass by Reference String Interpolation Named Arguments PHP File Upload Variadic Functions Return Values Search in Strings Multibyte Strings Type Declarations in Functions PHP Numbers PHP Math PHP Floats PHP Random Numbers PHP Constants PHP Properties PHP Methods PHP Constructor Constructor Property Promotion PHP Cookies PHP Destructor PHP Access Modifiers Readonly Properties PHP Sessions PHP Inheritance Method Overriding PHP Filters The final Keyword CSRF Protection PHP Filters Advanced Session Security Secure Cookies PHP Magic Constants File Upload Security PHP Callback Functions PHP Operators Filesystem Security PHP JSON Arithmetic Operators Error Reporting in Production Hiding PHP Assignment Operators PHP Configuration (php.ini) Comparison Operators PHP Command Line (CLI) PHP Exceptions Increment / Decrement Operators Migrating to PHP 8 PHP Form Validation PHP Form Required PHP Form URL/E-mail Error Handling Form Input Sanitizing Escaping Output (htmlspecialchars) Custom Error Handler Checkbox, Radio and Select Inputs Sticky Forms PHP Form Complete PHP Date and Time Date Formatting
Ask about this post
AI Ask about this post

Ask questions about Search in Strings and get answers drawn from it.

Signed-in readers only.