Signature
str_starts_with(string $haystack, string $needle): boolPurpose
str_starts_with() answers a simple question: does one string begin with another? It returns a boolean, so it fits directly into if conditions. Typical uses include checking URL path prefixes, recognizing a scheme in an identifier, or detecting a token type in a header value. Before PHP 8 developers usually wrote this check with substr() or strpos(), which was easy to get subtly wrong. This function states the intent directly.
Signature
Parameters and defaults
$haystack: the string to inspect.$needle: the prefix to look for at the very beginning of$haystack.
Neither parameter has a default value, so both are required. The comparison is case-sensitive and works on raw bytes, which means the function is binary-safe: strings containing null bytes are handled like any other string.
Return value and failure cases
The function returns true when $haystack begins with $needle, and false otherwise. It does not return an integer position or -1; it only ever returns a boolean. A needle that is longer than the haystack can never be a prefix, so the result is false. An empty needle is a prefix of every string, including the empty string, so the result is true.
The function does not signal a lookup failure with a special value. If you pass a value that cannot be used as a string, such as an array, PHP raises a TypeError.
Example 1: case sensitivity and the empty needle
The first example shows the basic behavior and the edge cases around empty strings.
$title = 'The lazy fox';
echo json_encode([
str_starts_with($title, 'The'),
str_starts_with($title, 'the'),
str_starts_with($title, ''),
str_starts_with('', ''),
str_starts_with('', 'a'),
]);
echo "\n";[true,false,true,true,false]
Only the exact-case prefix matches. The empty needle matches everything, even an empty haystack, while a non-empty needle cannot match an empty haystack.
Example 2: routing by path prefix
A common use is branching on the start of a path. Notice that the prefixes include the slashes, so the check is precise about where the segment begins.
function classify(string $path): string
{
if (str_starts_with($path, '/api/')) {
return 'api';
}
if (str_starts_with($path, '/admin/')) {
return 'admin';
}
return 'public';
}
$paths = ['/api/users', '/admin/', '/about', 'api/users', '/API/users'];
$result = [];
foreach ($paths as $path) {
$result[$path] = classify($path);
}
echo json_encode($result, JSON_UNESCAPED_SLASHES), "\n";{"/api/users":"api","/admin/":"admin","/about":"public","api/users":"public","/API/users":"public"}
The path api/users has no leading slash and /API/users differs in case, so both fall through to public.
Example 3: removing a prefix safely
Often you want to check for a prefix and then strip it. Guarding with str_starts_with() avoids cutting characters from strings that do not actually start with the prefix.
function removePrefix(string $value, string $prefix): string
{
if ($prefix !== '' && str_starts_with($value, $prefix)) {
return substr($value, strlen($prefix));
}
return $value;
}
$header = 'Bearer abc123';
echo json_encode([
removePrefix($header, 'Bearer '),
removePrefix($header, 'Basic '),
str_starts_with(strtolower('BEARER abc'), 'bearer '),
str_starts_with("\0abc", "\0"),
]);
echo "\n";["abc123","Bearer abc123",true,true]
The third check shows the usual way to get a case-insensitive comparison: normalize both sides first. With ASCII text, strtolower() is sufficient. The last check shows that a null byte is treated as an ordinary byte.
Edge cases
- Empty needle: always
true. If user input can be empty and you do not want that to count as a match, test for it explicitly, asremovePrefix()does above. - Needle longer than haystack: always
false. - Identical strings: a string starts with itself, so the result is
true. - Bytes, not characters: the comparison happens byte by byte. With valid UTF-8 on both sides, a prefix match still lines up with character boundaries, but the function does no Unicode case folding or normalization.
- Leading whitespace: a space or newline at the beginning is part of the string.
' http'does not start with'http'.
Common mistakes
- Expecting case-insensitive matching.
str_starts_with('Hello', 'he')isfalse. Lowercase both strings, or use another approach, when case should not matter. - Forgetting the empty needle. A variable that happens to be an empty string makes the check succeed for every haystack.
- Swapping the arguments. The haystack comes first and the prefix second. Reversing them tests the opposite relationship and often returns
falsesilently. - Using a loose prefix. Checking for
'/admin'also matches/administrator. Include the trailing separator, such as'/admin/', when the boundary matters. - Treating the result as a position. Older code using
strpos($s, $p) === 0compares an integer; here you simply use the boolean. - Passing arrays. Passing a non-string value like an array raises a
TypeErrorinstead of returningfalse.
Related functions
str_ends_with(): checks the end of a string instead of the start.str_contains(): checks whether a substring appears anywhere.stripos(): finds a case-insensitive position.substr(): extracts or removes part of a string once you know the prefix is present.strstr()andstrpbrk(): find the first occurrence of a string or character set.preg_match(): handles patterns that go beyond a fixed prefix.
Recap
str_starts_with() is a case-sensitive, binary-safe prefix test that returns true or false. An empty needle always matches, a longer needle never does, and you must normalize case yourself when you want a case-insensitive check. Pair it with substr() when you need to remove the prefix, and include separators in your prefixes to avoid accidental partial matches.
Tested examples
$title = 'The lazy fox';
echo json_encode([
str_starts_with($title, 'The'),
str_starts_with($title, 'the'),
str_starts_with($title, ''),
str_starts_with('', ''),
str_starts_with('', 'a'),
]);
echo "\n";[true,false,true,true,false]
function classify(string $path): string
{
if (str_starts_with($path, '/api/')) {
return 'api';
}
if (str_starts_with($path, '/admin/')) {
return 'admin';
}
return 'public';
}
$paths = ['/api/users', '/admin/', '/about', 'api/users', '/API/users'];
$result = [];
foreach ($paths as $path) {
$result[$path] = classify($path);
}
echo json_encode($result, JSON_UNESCAPED_SLASHES), "\n";{"/api/users":"api","/admin/":"admin","/about":"public","api/users":"public","/API/users":"public"}
function removePrefix(string $value, string $prefix): string
{
if ($prefix !== '' && str_starts_with($value, $prefix)) {
return substr($value, strlen($prefix));
}
return $value;
}
$header = 'Bearer abc123';
echo json_encode([
removePrefix($header, 'Bearer '),
removePrefix($header, 'Basic '),
str_starts_with(strtolower('BEARER abc'), 'bearer '),
str_starts_with("\0abc", "\0"),
]);
echo "\n";["abc123","Bearer abc123",true,true]