Slicing Strings
What Is String Slicing? In simple terms: Slicing means taking a piece out of a string — the first few characters, the last few, or a section in the middle. Technically: Slicing extracts a substring :
What Is String Slicing?
In simple terms: Slicing means taking a piece out of a string — the first few characters, the last few, or a section in the middle.
Technically: Slicing extracts a substring: a contiguous part of a string defined by a starting position and a length. PHP's main slicing function is substr() (byte-based), with mb_substr() as the character-based version for multibyte text. Other functions extract parts based on a delimiter instead of a position.
Why Slicing Is Needed
- Showing a short preview of a long description
- Extracting the year from a date string like
2026-10-08 - Getting the domain from an email address
- Reading fixed-width fields from file formats or log lines
- Showing only the last four digits of an account number
- Getting a file extension
How Positions Work
Positions in a string start at 0. Negative positions count backward from the end.
String: H e l l o W o r l d
Position: 0 1 2 3 4 5 6 7 8 9 10
Negative: -11 -10 -9 -8 -7 -6 -5 -4 -3 -2 -1
"Hello World" has 11 characters. The first is at position 0, the last at position 10 (or -1).
substr()
Syntax
substr(string $string, int $offset, ?int $length = null): string
| Parameter | Meaning |
|---|---|
$string | The source string |
$offset | Where to start. Positive: from the beginning. Negative: from the end. |
$length | Optional. Positive: how many characters to take. Negative: stop that many characters before the end. Omitted or null: take everything to the end. |
Example
<?php
$text = "Hello World";
echo substr($text, 0, 5) . "\n"; // from position 0, take 5
echo substr($text, 6) . "\n"; // from position 6 to the end
echo substr($text, -5) . "\n"; // last 5 characters
echo substr($text, -5, 3) . "\n"; // start 5 from the end, take 3
echo substr($text, 0, -6) . "\n"; // from start, stop 6 before the end
Output
Hello
World
World
Wor
Hello
Step-by-Step Explanation
| Call | Start | Length Rule | Result |
|---|---|---|---|
substr($text, 0, 5) | H (0) | Take 5 | Hello |
substr($text, 6) | W (6) | To the end | World |
substr($text, -5) | W (-5) | To the end | World |
substr($text, -5, 3) | W (-5) | Take 3 | Wor |
substr($text, 0, -6) | H (0) | Drop the last 6 | Hello |
A negative length is useful when you know how much to remove from the end rather than how much to keep — for example, removing a fixed suffix.
Out-of-Range Values
<?php
var_dump(substr("Hello", 10));
var_dump(substr("Hello", 2, 100));
var_dump(substr("Hello", -100, 2));
Output
string(0) ""
string(3) "llo"
string(2) "He"
- Starting beyond the end returns an empty string.
- A length that goes past the end simply stops at the end.
- A negative offset larger than the string starts at position 0.
Version note: Before PHP 8.0,
substr()returnedfalsewhen the offset was beyond the end. Since PHP 8.0, it always returns a string (empty when nothing is extracted). Old code that checkssubstr(...) === falseno longer detects this case; check for""instead.
Getting the First and Last Characters
<?php
$code = "INV2026";
echo substr($code, 0, 1) . "\n"; // first character
echo $code[0] . "\n"; // same, using offset syntax
echo substr($code, -1) . "\n"; // last character
echo $code[-1] . "\n"; // same, PHP 7.1+
echo substr($code, 0, 3) . "\n"; // prefix
echo substr($code, 3) . "\n"; // everything after the prefix
Output
I
I
6
6
INV
2026
$code[0] and $code[-1] are convenient for single characters, but they raise a warning if the string is empty or too short. substr() returns an empty string instead, which is safer when the input is not guaranteed.
Practical Use: Fixed-Position Data
Some formats place fields at fixed positions. Slicing reads them directly.
<?php
$filename = "invoice_2026-10-08_1042.pdf";
$date = substr($filename, 8, 10);
$invoiceNo = substr($filename, 19, 4);
$year = substr($date, 0, 4);
echo "Date: $date\n";
echo "Invoice: $invoiceNo\n";
echo "Year: $year\n";
Output
Date: 2026-10-08
Invoice: 1042
Year: 2026
How It Works
invoice_is 8 characters long, so the date starts at position8and has 10 characters.- After the date comes
_(position 18), so the invoice number starts at position19.
Fixed positions are fragile: if the format changes (for example, a different prefix), every offset becomes wrong. When the data has delimiters, splitting by the delimiter is more robust:
<?php
[$type, $date, $rest] = explode("_", "invoice_2026-10-08_1042.pdf");
$invoiceNo = pathinfo($rest, PATHINFO_FILENAME);
echo "$type | $date | $invoiceNo";
Output
invoice | 2026-10-08 | 1042
The square brackets on the left destructure the array returned by explode() into three variables (see Array Destructuring). pathinfo(..., PATHINFO_FILENAME) returns the name without its extension.
Slicing by Delimiter: strstr() and strrchr()
When you do not know the position but do know a character that separates the parts, use functions that slice around that character.
strstr() — From the First Occurrence
strstr(string $haystack, string $needle, bool $before_needle = false): string|false
<?php
$email = "customer.support@gocourse.in";
echo strstr($email, "@") . "\n";
echo strstr($email, "@", true) . "\n";
echo substr(strstr($email, "@"), 1) . "\n";
Output
@gocourse.in
customer.support
gocourse.in
strstr($email, "@")returns everything from the first@to the end, including the@.- With
trueas the third argument, it returns everything before the@. substr(..., 1)removes the leading@to get just the domain.
strstr() returns false if the needle is not found. Check for that before using the result, or validate the input first.
strrchr() — From the Last Occurrence
<?php
$file = "report.final.v2.pdf";
echo strrchr($file, ".") . "\n";
echo pathinfo($file, PATHINFO_EXTENSION) . "\n";
Output
.pdf
pdf
strrchr() finds the last occurrence of a character and returns everything from there. Because file names can contain several dots, the last one is the one that matters for the extension. For file paths specifically, pathinfo() is clearer: it returns the extension without the dot and also handles directory names.
Extracting Text Between Two Markers
Combining strpos() with substr() extracts a section between two delimiters:
<?php
function between(string $text, string $start, string $end): ?string
{
$startPos = strpos($text, $start);
if ($startPos === false) {
return null;
}
$startPos += strlen($start);
$endPos = strpos($text, $end, $startPos);
if ($endPos === false) {
return null;
}
return substr($text, $startPos, $endPos - $startPos);
}
var_dump(between("Order ID: [A-1042] confirmed", "[", "]"));
var_dump(between("No brackets here", "[", "]"));
Output
string(6) "A-1042"
NULL
Step-by-Step Explanation
strpos($text, $start)finds the position of[(position 10).- Adding
strlen($start)moves past the marker, so extraction starts atA(position 11). strpos($text, $end, $startPos)searches for]starting after the opening marker, so an earlier]cannot be matched by mistake.- The length is the distance between the two positions:
$endPos - $startPos. - If either marker is missing, the function returns
nullinstead of a wrong slice. Note the strict=== falsechecks — a marker at position0is valid.
For more complex extraction, regular expressions with preg_match() are often simpler (see preg_match).
Slicing Multibyte Text: mb_substr()
The Problem
substr() counts bytes. In UTF-8, characters such as é, ₹, and Tamil letters use multiple bytes. Cutting through a character produces invalid text.
<?php
$drink = "Café Mocha";
var_dump(substr($drink, 0, 4));
var_dump(mb_substr($drink, 0, 4));
Output
string(4) "Caf�"
string(5) "Café"
Why
é is stored as two bytes. substr($drink, 0, 4) takes C, a, f, and only the first byte of é. That lone byte is not a valid character, so it is displayed as � (the replacement character). mb_substr() counts characters, so it returns all of é — 4 characters, 5 bytes.
Syntax
mb_substr(string $string, int $start, ?int $length = null, ?string $encoding = null): string
The parameters work like substr(), including negative values, but positions are in characters. The encoding defaults to UTF-8 (the mbstring default).
Rule
Use mb_substr() for any text that a person writes or reads: names, titles, messages, comments, and descriptions. Use substr() for data that is guaranteed to be ASCII, such as dates in YYYY-MM-DD format, hexadecimal hashes, or fixed technical codes.
Truncating Text for Display: mb_strimwidth()
mb_strimwidth() shortens text to a maximum display width and adds a marker such as ...:
<?php
$title = "Learning PHP step by step";
echo mb_strimwidth($title, 0, 15, "...") . "\n";
echo mb_strimwidth("Short", 0, 15, "...") . "\n";
Output
Learning PHP...
Short
How It Works
- The width (
15) includes the trim marker. The result is 12 characters of text plus.... - If the text already fits, it is returned unchanged, without a marker.
- It measures display width: East Asian full-width characters count as 2, which keeps columns aligned in fixed-width output.
mb_strimwidth() may cut in the middle of a word. For word-aware truncation, cut with mb_substr() and then shorten to the last space, as shown in the excerpt() example in String Functions.
Splitting Into Equal Pieces: str_split()
<?php
$card = "4111111111111111";
echo implode(" ", str_split($card, 4)) . "\n";
$hex = "ff8800";
[$r, $g, $b] = array_map('hexdec', str_split($hex, 2));
echo "R: $r, G: $g, B: $b\n";
Output
4111 1111 1111 1111
R: 255, G: 136, B: 0
str_split($card, 4)divides the string into 4-character chunks, which are joined with spaces — the familiar card number format.- For the color, the hex string is split into 2-character chunks (
ff,88,00), andhexdec()converts each to a decimal number.
Use mb_str_split() for multibyte text.
Splitting at the First Delimiter: explode() With a Limit
When only the first occurrence of a delimiter matters, explode() with a limit of 2 splits the string into exactly two parts:
<?php
$line = "token=abc=123";
[$key, $value] = explode("=", $line, 2);
echo "Key: $key\n";
echo "Value: $value\n";
Output
Key: token
Value: abc=123
Without the limit, the value would be split again at its own = sign, and abc=123 would be broken into two parts. This pattern is used for parsing configuration lines, HTTP headers (Name: value), and query-style data.
Choosing the Right Tool
| You Know | Use |
|---|---|
| Exact position and length (ASCII data) | substr() |
| Exact position and length (human text) | mb_substr() |
| A delimiter; you want the part after or before its first occurrence | strstr() (with true for before) |
| A delimiter; you want the part after its last occurrence | strrchr() |
| Start and end markers | strpos() + substr(), or preg_match() |
| A delimiter separating several fields | explode() |
| A maximum display length | mb_strimwidth() |
| A fixed chunk size | str_split() / mb_str_split() |
| A file path | pathinfo(), basename(), dirname() |
Common Mistakes
Using substr() on Multibyte Text
Effect: Broken characters (�) and invalid UTF-8, which can also break JSON encoding.
Fix: Use mb_substr() for human-readable text.
Confusing Length With End Position
<?php
// Intention: characters from position 6 to position 8
echo substr("Hello World", 6, 8);
Effect: Prints World — the third argument is a length (8 characters), not an end position, so it takes everything available.
Fix: Calculate the length: substr($s, 6, 8 - 6 + 1) for positions 6 through 8 inclusive (Wor).
Not Handling a Missing Delimiter
Effect: strstr() or strpos() returns false, and passing false to substr() produces wrong results or errors.
Fix: Check with === false before slicing, or use str_contains() first.
Checking substr() for false in PHP 8
Effect: The check never triggers, because PHP 8 returns "".
Fix: Check for an empty string or validate the length beforehand.
Using rtrim() to Remove a Suffix
Effect: rtrim("lessons.json", ".json") returns "le", not "lessons". The second argument is a list of characters, so PHP keeps removing any trailing ., j, s, o, or n — including the ones that belong to the word itself.
Fix: str_ends_with($s, $suffix) ? substr($s, 0, -strlen($suffix)) : $s.
Best Practices
- Use
mb_substr()andmb_strimwidth()for user-facing text; usesubstr()for ASCII data. - Prefer delimiter-based extraction (
explode(),strstr(),pathinfo()) over hard-coded positions when the format has separators. - Check search results with
=== falsebefore using them as positions. - Use
explode()with a limit when only the first delimiter should split the string. - Keep slicing logic in small, named functions (
between(),excerpt()) so it can be tested and reused.
Official reference: https://www.php.net/manual/en/function.substr.php