Search in Strings
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:
| Question | Functions | Return |
|---|---|---|
| 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_* versions | Position (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 inifstatements.- The search is case-sensitive:
Browndoes not matchbrown.
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..PDFdoes not match.pdfbecause both functions are case-sensitive. Lowercasing the file name first makes the check reliable.
Practical Uses
| Check | Example |
|---|---|
| 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 namedvirus.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
($pos = strpos(...)) !== falseassigns the result to$posand checks it in one expression. The parentheses are required so that the assignment happens before the comparison.- Each found position is stored.
- The next search starts after the current match (
$pos + strlen($needle)), which prevents finding the same occurrence again. - The loop ends when
strpos()returnsfalse. - The empty-needle guard prevents an endless loop.
Other Position Functions
| Function | Finds | Case |
|---|---|---|
strpos() | First occurrence | Sensitive |
stripos() | First occurrence | Insensitive (ASCII) |
strrpos() | Last occurrence | Sensitive |
strripos() | Last occurrence | Insensitive (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, soWORLDmatchesWorldat 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 inreport.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(), andstr_ends_with()work correctly with UTF-8 text, because they compare complete byte sequences and return onlytrueorfalse.
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()findscattwice: once insidecategoryand once as a word.\bin a regular expression marks a word boundary, so/\bcat\b/imatches only the standalone word. Theimodifier 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()
- The query is trimmed and lowercased with
mb_strtolower(), so' MOUSE 'becomes'mouse'. - An empty query returns no results instead of matching everything.
array_filter()keeps products whose lowercased name contains the query;array_values()re-numbers the results from0.
highlight()
- The text and the term are escaped with
htmlspecialchars()before highlighting, so the output is safe HTML (notice&becomes&). preg_quote()escapes characters in the term that have special meaning in regular expressions, such as.or+, so user input cannot change the pattern.- The
imodifier makes the match case-insensitive;umakes the pattern work with UTF-8 text. $0in 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(), andstr_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 withmb_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