Introduction
PHP 8.0 introduced built-in str_starts_with() and str_ends_with() functions that check whether a string begins or ends with a given substring. Before PHP 8.0, developers had to implement these functions manually using substr(), strpos(), or strncmp(). Both functions are case-sensitive, perform binary-safe comparisons, and return true or false. This article covers the PHP 8.0+ built-in functions, pre-PHP 8 polyfills, and related string checking patterns.
PHP 8.0+ Built-in Functions
1// str_starts_with(string $haystack, string $needle): bool
2echo str_starts_with("Hello World", "Hello"); // true
3echo str_starts_with("Hello World", "World"); // false
4echo str_starts_with("Hello World", ""); // true (empty needle)
5
6// str_ends_with(string $haystack, string $needle): bool
7echo str_ends_with("Hello World", "World"); // true
8echo str_ends_with("Hello World", "Hello"); // false
9echo str_ends_with("Hello World", ""); // true (empty needle)
10
11// Practical examples
12$url = "https://example.com/api/users";
13if (str_starts_with($url, "https://")) {
14 echo "Secure connection";
15}
16
17$filename = "report.pdf";
18if (str_ends_with($filename, ".pdf")) {
19 echo "PDF file detected";
20}
Both functions return true for an empty needle — every string starts and ends with the empty string.
Also Added in PHP 8.0: str_contains()
1// str_contains(string $haystack, string $needle): bool
2echo str_contains("Hello World", "lo Wo"); // true
3echo str_contains("Hello World", "xyz"); // false
4
5// Before PHP 8.0, this required strpos
6if (strpos("Hello World", "lo Wo") !== false) {
7 // Found
8}
PHP 8.0 added all three functions together: str_starts_with(), str_ends_with(), and str_contains().
Pre-PHP 8.0 Polyfills
If you must support PHP 7.x, implement these functions manually:
1if (!function_exists('str_starts_with')) {
2 function str_starts_with(string $haystack, string $needle): bool {
3 return strncmp($haystack, $needle, strlen($needle)) === 0;
4 }
5}
6
7if (!function_exists('str_ends_with')) {
8 function str_ends_with(string $haystack, string $needle): bool {
9 if ($needle === '') {
10 return true;
11 }
12 return substr($haystack, -strlen($needle)) === $needle;
13 }
14}
15
16if (!function_exists('str_contains')) {
17 function str_contains(string $haystack, string $needle): bool {
18 return $needle === '' || strpos($haystack, $needle) !== false;
19 }
20}
strncmp() is the most efficient for prefix checking because it compares only the first N characters without extracting a substring.
Alternative Implementations (Pre-PHP 8)
1// Using substr()
2function startsWith(string $haystack, string $needle): bool {
3 return substr($haystack, 0, strlen($needle)) === $needle;
4}
5
6function endsWith(string $haystack, string $needle): bool {
7 $len = strlen($needle);
8 if ($len === 0) return true;
9 return substr($haystack, -$len) === $needle;
10}
11
12// Using strpos() — less efficient for starts_with
13function startsWithStrpos(string $haystack, string $needle): bool {
14 return strpos($haystack, $needle) === 0;
15}
16
17// Using regex — most flexible but slowest
18function startsWithRegex(string $haystack, string $needle): bool {
19 return preg_match('/^' . preg_quote($needle, '/') . '/', $haystack) === 1;
20}
Case-Insensitive Variants
The built-in functions are case-sensitive. For case-insensitive matching:
1// Case-insensitive starts with
2function str_starts_with_ci(string $haystack, string $needle): bool {
3 return str_starts_with(strtolower($haystack), strtolower($needle));
4}
5
6// Case-insensitive ends with
7function str_ends_with_ci(string $haystack, string $needle): bool {
8 return str_ends_with(strtolower($haystack), strtolower($needle));
9}
10
11echo str_starts_with_ci("Hello World", "hello"); // true
12echo str_ends_with_ci("report.PDF", ".pdf"); // true
13
14// Or use strncasecmp for prefix (more efficient)
15function starts_with_case_insensitive(string $haystack, string $needle): bool {
16 return strncasecmp($haystack, $needle, strlen($needle)) === 0;
17}
Practical Examples
1// URL routing
2function routeRequest(string $path): string {
3 if (str_starts_with($path, "/api/")) {
4 return "API handler";
5 } elseif (str_starts_with($path, "/admin/")) {
6 return "Admin handler";
7 }
8 return "Default handler";
9}
10
11// File type detection
12function getFileType(string $filename): string {
13 return match(true) {
14 str_ends_with($filename, ".jpg"),
15 str_ends_with($filename, ".png") => "image",
16 str_ends_with($filename, ".pdf") => "document",
17 str_ends_with($filename, ".mp4") => "video",
18 default => "unknown"
19 };
20}
21
22// Checking multiple prefixes
23function hasValidProtocol(string $url): bool {
24 $protocols = ["http://", "https://", "ftp://"];
25 foreach ($protocols as $protocol) {
26 if (str_starts_with($url, $protocol)) {
27 return true;
28 }
29 }
30 return false;
31}
32
33// String cleaning
34function removePrefix(string $str, string $prefix): string {
35 if (str_starts_with($str, $prefix)) {
36 return substr($str, strlen($prefix));
37 }
38 return $str;
39}
40
41echo removePrefix("/api/v1/users", "/api/v1"); // "/users"
Common Pitfalls
Using strpos() === 0 instead of str_starts_with() on PHP 8+: While strpos($haystack, $needle) === 0 works for prefix checking, it scans the entire string and is less readable. str_starts_with() stops after comparing the prefix length and communicates intent more clearly.
Forgetting strict comparison with strpos(): In pre-PHP 8 code, if (strpos($str, $needle)) is wrong because strpos() returns 0 (falsy) when the needle is at position 0. You must use strpos($str, $needle) !== false with strict comparison.
Not handling empty needle: Both str_starts_with() and str_ends_with() return true for empty needles. If your logic should reject empty inputs, check $needle !== '' before calling these functions.
Using str_starts_with() on PHP versions below 8.0 without a polyfill: Calling str_starts_with() on PHP 7.x throws Call to undefined function. Either add a polyfill or check your PHP version with PHP_VERSION_ID >= 80000.
Assuming case-insensitive behavior: str_starts_with("Hello", "hello") returns false. Use strtolower() on both strings or strncasecmp() if you need case-insensitive matching.
Summary
PHP 8.0 added str_starts_with(), str_ends_with(), and str_contains() as built-in functions
All three are case-sensitive and return true for empty needles
For PHP 7.x, use polyfills based on strncmp() (prefix) and substr() (suffix)
Use strncasecmp() or strtolower() for case-insensitive matching
Prefer str_starts_with() over strpos() === 0 for readability and performance
Always use strict comparison (!== false) with strpos() in pre-PHP 8 code