Signature
substr(string $string, int $offset, ?int $length = null): stringPurpose
substr() returns a slice of a string. You choose where the slice starts with offset and, optionally, how long it is with length. It is the standard tool for taking a prefix, a suffix, or a fixed window from a string whose layout you already know, such as the last four digits of an identifier or the first characters of a title.
Signature
Parameters and defaults
$string: the input. Positions are counted in bytes, starting from zero.$offset: where the slice begins. A non-negative value counts from the start. A negative value counts back from the end, so-1is the last byte. If the offset is larger than the string, the result is an empty string. If a negative offset reaches back past the start, it is treated as the start of the string.$length: optional, defaultnull. A positive value gives at most that many bytes. A negative value omits that many bytes from the end of the string. If the end point lands at or before the start position, the result is an empty string. A length of0returns an empty string. Omitting it, or passingnull, returns everything from the offset to the end.
Return value and failure cases
substr() always returns a string. Since PHP 8.0.0 an invalid range gives an empty string rather than false, so you no longer need a false check. Also since 8.0.0, length is nullable, and an explicit null means "to the end of the string". Before that, null produced an empty string.
Out-of-range numbers do not raise errors. A TypeError occurs only when an argument cannot be used as the required type, for example an array passed as the string. In ordinary non-strict mode, scalars are converted: substr(54321, 0, 2) gives '54', and substr(false, 0, 1) gives ''. An object with __toString() is also accepted. Under declare(strict_types=1), passing an integer as the string is a type error.
Example 1: offsets and lengths
The first example shows a start offset, a length, and negative offsets counted from the end.
$s = 'abcdef';
echo json_encode([
substr($s, 1),
substr($s, 1, 3),
substr($s, -2),
substr($s, -3, 1),
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), PHP_EOL;["bcdef","bcd","ef","d"]
Example 2: negative length and empty results
A negative length trims from the right. When the range is empty or invalid, you get "".
$s = 'abcdef';
echo json_encode([
substr($s, 0, -1),
substr($s, 2, -1),
substr($s, 4, -4),
substr($s, -3, -1),
substr($s, 10),
substr($s, 1, null),
substr($s, 2, 0),
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), PHP_EOL;["abcde","cde","","de","","bcdef",""]
The third result is empty because the end point (position 2) lies before the start (position 4). The fifth is empty because the offset is beyond the end of the string.
Example 3: masking and previews
Two small helpers show everyday use. The first keeps the last four characters and masks the rest. The second shortens long text.
function maskCard(string $number): string
{
$last4 = substr($number, -4);
return str_repeat('*', max(0, strlen($number) - 4)) . $last4;
}
function preview(string $text, int $max): string
{
return strlen($text) > $max ? substr($text, 0, $max) . '...' : $text;
}
echo json_encode([
maskCard('4111111111111111'),
maskCard('123'),
preview('PHP substring basics', 10),
preview('short', 10),
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), PHP_EOL;["************1111","123","PHP substr...","short"]
Notice that maskCard('123') still works. The negative offset -4 reaches past the start of a three-byte string, so PHP begins at the start and returns 123.
Edge cases
- Empty input:
substr('', 0, 1)returns an empty string. - Offset equal to the length: the result is an empty string, because there is nothing left to take.
- Length longer than the remainder: the result is simply shortened, with no padding.
- Single characters: for one byte you can also use square brackets, such as
$string[0]. Unlikesubstr(), reading an index that does not exist produces a warning instead of an empty string.
Common mistakes
- Treating bytes as characters.
substr()counts bytes. In UTF-8 text, characters such as accented letters or emoji use several bytes, and cutting in the middle produces invalid text. Usemb_substr()when the input may contain multibyte characters. - Mixing up length and end position. The third argument is a count, not an index.
substr('abcdef', 2, 3)returnscde, not the characters up to position 3. - Expecting
falseon failure. That was the behaviour before PHP 8.0.0. Checking=== falsetoday never triggers. - Forgetting what a negative length means. It removes characters from the end. It does not reverse direction.
- Using it to test for a prefix. For a simple check, a dedicated function such as
str_starts_with()states the intent more clearly than comparing a slice.
Related functions
mb_substr(): the multibyte-aware version.substr_replace(): replaces a portion of a string.strrchr(): finds the last occurrence of a character and returns the rest of the string.trim(): removes whitespace or characters from the ends.wordwrap(): wraps text at a given width.preg_match(): extracts text by pattern rather than by position.
Recap
substr($string, $offset, $length) slices by byte position. Negative offsets count from the end, and negative lengths trim from the end. A null or omitted length means "to the end". Invalid ranges return "" in PHP 8.4, and multibyte text needs mb_substr().
Tested examples
$s = 'abcdef';
echo json_encode([
substr($s, 1),
substr($s, 1, 3),
substr($s, -2),
substr($s, -3, 1),
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), PHP_EOL;["bcdef","bcd","ef","d"]
$s = 'abcdef';
echo json_encode([
substr($s, 0, -1),
substr($s, 2, -1),
substr($s, 4, -4),
substr($s, -3, -1),
substr($s, 10),
substr($s, 1, null),
substr($s, 2, 0),
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), PHP_EOL;["abcde","cde","","de","","bcdef",""]
function maskCard(string $number): string
{
$last4 = substr($number, -4);
return str_repeat('*', max(0, strlen($number) - 4)) . $last4;
}
function preview(string $text, int $max): string
{
return strlen($text) > $max ? substr($text, 0, $max) . '...' : $text;
}
echo json_encode([
maskCard('4111111111111111'),
maskCard('123'),
preview('PHP substring basics', 10),
preview('short', 10),
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), PHP_EOL;["************1111","123","PHP substr...","short"]