substr_compare

Compares part of one string with another string, starting at a given offset, without copying it.

Signature
substr_compare(string $haystack, string $needle, int $offset, ?int $length = null, bool $case_insensitive = false): int
  • Added in PHP 5.0

Parameters

ParameterTypeDescription
$haystackstringThe main string being compared.
$needlestringThe secondary string being compared against the haystack portion.
$offsetintThe position in haystack where the comparison starts. Negative values count back from the end of the string.
$lengthoptional?int= nullHow many bytes to compare. When null, the length used is the larger of the needle length and the remaining haystack length after offset.
$case_insensitiveoptionalbool= falseIf true, the comparison ignores ASCII case.

Return value

int0 if the two compared parts are equal, a negative number if the haystack portion sorts before the needle, and a positive number if it sorts after.

Errors

Throws a ValueError if offset is greater than the length of haystack, or if length is negative.

Changelog

  • PHP 8.2.0The function is no longer guaranteed to return strlen($haystack) - strlen($needle) when the lengths differ; it may return -1 or 1 instead. Compare against 0, not against a specific number.
  • PHP 8.0.0length is nullable now.
  • PHP 7.2.18, 7.3.5offset may now be equal to the length of haystack, which previously raised a warning.

substr_compare compares a slice of haystack against needle without allocating that slice first. It returns an strcmp-style integer: zero means equal, negative means the haystack part sorts first.

When to reach for it

Its niche is checking a prefix or suffix on large strings in a hot loop, where substr($s, -4) === '.csv' would copy bytes on every iteration. It is also the only core function that does an offset-based comparison with an optional case-insensitive flag in one call, which is convenient for parsing fixed-layout records where a field must match a constant.

For plain readability though, str_starts_with and str_ends_with (PHP 8.0+) are clearer for the common cases and should be your default.

What trips people up

Only the sign of the result is meaningful. Since PHP 8.2 the function may return -1 or 1 instead of the length difference, so any code doing arithmetic on the return value, or comparing it to a specific integer, is broken. Always test === 0 or < 0.

An offset beyond the end of the haystack throws a ValueError rather than returning something — validate the length first if the offset is computed.

Example

Try it yourself
Loading editor…
Press Run to execute the code.

When to use it

  • Check whether a filename ends with a given extension without copying the tail of the string.
  • Verify a fixed-position record type code in a flat-file line during a large import.
  • Do a case-insensitive prefix check on a header value without allocating a lowercased copy.

Frequently asked questions

What is the difference between substr_compare and str_ends_with?

str_ends_with is simpler and returns a bool, but it has no offset or case-insensitive option. substr_compare can compare at any offset, limit the comparison length, and fold case, and it avoids allocating a substring.

Why does substr_compare return 1 instead of the length difference?

As of PHP 8.2 the exact magnitude is no longer guaranteed; only the sign is defined. Test the result with === 0, < 0 or > 0.

Does substr_compare throw if the offset is too large?

Yes — an offset greater than the haystack length throws a ValueError. An offset exactly equal to the length has been allowed since PHP 7.2.18 and 7.3.5.

More examples

Reading a fixed-layout record

Prints 'other row: HDR', 'detail row: 000123widget', 'other row: TRL'. The comparison reads the three-byte record type in place, which matters when the loop runs over millions of lines.

Example · php
<?php
$lines = ['HDR20240101ACME', 'DTL000123widget', 'TRL000002'];

foreach ($lines as $line) {
    if (substr_compare($line, 'DTL', 0, 3) === 0) {
        echo 'detail row: ', substr($line, 3), "\n";
    } else {
        echo 'other row: ', substr($line, 0, 3), "\n";
    }
}

Guarding against a ValueError

Prints bool(true) then bool(false). The length check is essential: without it the second call would pass an offset larger than the haystack and throw a ValueError instead of returning false.

Example · php
<?php
function endsWithCi(string $s, string $suffix): bool {
    $len = strlen($suffix);
    if ($len > strlen($s)) {
        return false;
    }
    return substr_compare($s, $suffix, -$len, $len, true) === 0;
}

var_dump(endsWithCi('archive.TAR.GZ', '.tar.gz')); // true
var_dump(endsWithCi('a', '.tar.gz'));              // false

Discussion

  • Be the first to comment on this lesson.