Strings7 min readExamples run on PHP 8.5.10

str_starts_with()

Learn how PHP 8.4 str_starts_with() checks whether a string begins with a prefix, including case sensitivity, empty needles, edge cases and common mistakes.

Signature

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

Purpose

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.

Example
<?php
$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";
Output
[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.

Example
<?php
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";
Output
{"/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.

Example
<?php
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";
Output
["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, as removePrefix() 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

  1. Expecting case-insensitive matching. str_starts_with('Hello', 'he') is false. Lowercase both strings, or use another approach, when case should not matter.
  2. Forgetting the empty needle. A variable that happens to be an empty string makes the check succeed for every haystack.
  3. Swapping the arguments. The haystack comes first and the prefix second. Reversing them tests the opposite relationship and often returns false silently.
  4. Using a loose prefix. Checking for '/admin' also matches /administrator. Include the trailing separator, such as '/admin/', when the boundary matters.
  5. Treating the result as a position. Older code using strpos($s, $p) === 0 compares an integer; here you simply use the boolean.
  6. Passing arrays. Passing a non-string value like an array raises a TypeError instead of returning false.
  • 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() and strpbrk(): 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

Case sensitivity and the empty needle
<?php
$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";
Output
[true,false,true,true,false]
Routing by path prefix
<?php
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";
Output
{"/api/users":"api","/admin/":"admin","/about":"public","api/users":"public","/API/users":"public"}
Removing a prefix safely
<?php
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";
Output
["abc123","Bearer abc123",true,true]