Signature
substr_compare(
string $haystack,
string $needle,
int $offset,
?int $length = null,
bool $case_insensitive = false
): intPurpose
substr_compare() compares a slice of one string with another string, without first creating the slice using substr(). You tell it where to start in the haystack, optionally how many bytes to compare, and whether letter case should be ignored. It is a natural fit for checks such as "does this filename end with .pdf?" or "does the text at position 4 match this keyword?".
The comparison is binary safe: it works on bytes, so embedded null bytes are treated like any other byte.
Signature
Parameters and defaults
$haystack: the main string. Comparison begins inside it.$needle: the string compared against the part of the haystack.$offset: the starting position in the haystack. A negative value counts from the end of the string.$length: how many bytes to compare. The defaultnullmeans the larger of the needle length and the haystack length minus the offset, so both remaining parts are compared in full.$case_insensitive: whentrue, letter case is ignored. The default isfalse.
Return value and failure cases
The function returns an int: a value below zero, zero, or a value above zero. Only the sign carries meaning. Since PHP 8.2 the result is no longer guaranteed to be the difference of the string lengths, and may be -1 or 1. If your code needs a stable result, normalize it with <=> 0.
The manual page text still mentions a warning and false for bad arguments, but the declared return type is int. In PHP 8 an $offset greater than the haystack length, or a negative $length, raises a ValueError. An offset exactly equal to the haystack length is allowed, and the haystack part is then empty. A $length of 0 compares nothing and returns 0.
Example 1: a case-insensitive suffix check
A negative offset makes suffix tests short. The helper guards two cases first: an empty suffix, and a suffix longer than the text. Without the second guard, a negative offset beyond the start is clamped to the start of the string, and the comparison would run against the whole text.
function endsWithIgnoreCase(string $text, string $suffix): bool
{
if ($suffix === '') {
return true;
}
if (strlen($suffix) > strlen($text)) {
return false;
}
return substr_compare($text, $suffix, -strlen($suffix), null, true) === 0;
}
echo json_encode([
endsWithIgnoreCase("Report.PDF", ".pdf"),
endsWithIgnoreCase("Report.txt", ".pdf"),
endsWithIgnoreCase("a", ".pdf"),
]), PHP_EOL;[true,false,false]
Example 2: the effect of length
The $length argument limits how much of each side is compared. With length 2, the extra g in bcg is never examined. With length 3, the haystack part bcd is longer than bc, so the haystack sorts after the needle. The example normalizes each result to -1, 0 or 1.
$results = [
substr_compare("abcde", "bc", 1, 2) <=> 0,
substr_compare("abcde", "bcg", 1, 2) <=> 0,
substr_compare("abcde", "BC", 1, 2, true) <=> 0,
substr_compare("abcde", "bc", 1, 3) <=> 0,
substr_compare("abcde", "cd", 1, 2) <=> 0,
];
echo json_encode($results), PHP_EOL;[0,0,0,1,-1]
Example 3: boundary offsets and errors
An offset equal to the haystack length is valid and compares an empty string with the needle. A larger offset throws ValueError. A zero length returns 0 regardless of the strings.
echo json_encode(substr_compare("abcde", "abc", 5, 1) <=> 0), PHP_EOL;
try {
substr_compare("abcde", "abc", 6);
} catch (ValueError $e) {
echo "ValueError", PHP_EOL;
}
echo json_encode(substr_compare("abcde", "zzz", 1, 0)), PHP_EOL;-1 ValueError 0
Edge cases
- Negative offsets count from the end. If the negative value reaches before the first byte, the start is clamped to the beginning of the string rather than failing.
- Default length with an empty needle: the remaining haystack is compared with an empty string, so the result is nonzero unless the remaining part is also empty.
- Length shorter than both parts: only that many bytes are compared, so strings that differ later still count as equal.
- Multibyte text: offsets and lengths are byte counts, not characters. A UTF-8 character can span several bytes, so an offset may land in the middle of one. Use ASCII data, or the
mb_functions, when characters matter.
Common mistakes
- Treating the result as a boolean.
0means equal, which is falsy. Test=== 0for equality. - Relying on exact numbers. Only the sign is meaningful, and PHP 8.2 changed the possible magnitudes. Compare against
0or use<=> 0. - Forgetting the length guard in suffix checks. A needle longer than the haystack combined with a negative offset can compare the wrong region.
- Passing an offset past the end. In PHP 8 this throws
ValueError, so validate the offset or catch the exception. - Expecting a search. The function compares at one position only. To find where a string occurs, use
strpos()or a related function.
Related functions
strncmp()compares the first n bytes of two strings.strcmp()andstrcasecmp()compare whole strings.substr()extracts a slice when you need the text itself.str_ends_with()andstr_starts_with()give a simpler case-sensitive prefix or suffix test.
Recap
Use substr_compare() to compare a byte range of the haystack with a needle. Choose the start with $offset, limit the work with $length, and ignore case with the last argument. Check the result with === 0 or normalize its sign, keep offsets within the string, and remember that all positions are measured in bytes.
Tested examples
function endsWithIgnoreCase(string $text, string $suffix): bool
{
if ($suffix === '') {
return true;
}
if (strlen($suffix) > strlen($text)) {
return false;
}
return substr_compare($text, $suffix, -strlen($suffix), null, true) === 0;
}
echo json_encode([
endsWithIgnoreCase("Report.PDF", ".pdf"),
endsWithIgnoreCase("Report.txt", ".pdf"),
endsWithIgnoreCase("a", ".pdf"),
]), PHP_EOL;[true,false,false]
$results = [
substr_compare("abcde", "bc", 1, 2) <=> 0,
substr_compare("abcde", "bcg", 1, 2) <=> 0,
substr_compare("abcde", "BC", 1, 2, true) <=> 0,
substr_compare("abcde", "bc", 1, 3) <=> 0,
substr_compare("abcde", "cd", 1, 2) <=> 0,
];
echo json_encode($results), PHP_EOL;[0,0,0,1,-1]
echo json_encode(substr_compare("abcde", "abc", 5, 1) <=> 0), PHP_EOL;
try {
substr_compare("abcde", "abc", 6);
} catch (ValueError $e) {
echo "ValueError", PHP_EOL;
}
echo json_encode(substr_compare("abcde", "zzz", 1, 0)), PHP_EOL;-1 ValueError 0