In PHP, how do $s[0] and $s[-1] read single characters of a string, and what happens with an offset past the end?
answer
- zero-based byte offsets
- negative offsets count from the end
- Uninitialized string offset warning
- $s{0} removed in 8.0
- writing past the end pads with spaces
basics
~20 s$s[0] returns the first byte of a string as a one-character string and $s[-1] the last. Reading past the end gives an E_WARNING, Uninitialized string offset, and an empty string. Offsets count bytes, not characters.
solid answer
~40 sA string can be indexed like an array: `$s[0]` is the first **byte**, returned as a one-character string, and negative offsets count from the end, so `$s[-1]` is the last byte. Reading an offset past either end raises `E_WARNING` "Uninitialized string offset" and evaluates to `''`; `isset($s[9])` or `$s[9] ?? ''` checks without a warning. You can also write: `$s[0] = 'J'` replaces one byte, writing past the end pads the gap with spaces, assigning `''` throws an `Error`, and assigning a longer string keeps only its first byte with a warning. The old `$s{0}` brace syntax was removed in PHP 8.0. Offsets are bytes, so on UTF-8 text such as `'Émile'`, `$s[0]` returns half of a character.
code
php · 19 lines<?php
declare(strict_types=1);
$ref = 'APT-2291';
echo $ref[0], ' ', $ref[-1], PHP_EOL; // A 1
echo "Check digit $ref[-1]\n"; // Check digit 1
echo $ref[20] ?? '?', PHP_EOL; // ? (no warning)
$missing = $ref[20]; // Warning: Uninitialized string offset 20, gives ''
$code = 'ab';
$code[4] = 'z';
var_dump($code); // string(5) "ab z"
try {
$code[0] = '';
} catch (Error $e) {
echo $e->getMessage(), PHP_EOL; // Cannot assign an empty string to a string offset
}go deeper
Recall that $s[0] is the first character as a string, $s[-1] the last, and that reading past the end warns and gives an empty string.
Explain the write rules: space padding, first-byte-only assignment, the empty-string Error, and why $s{0} fails in PHP 8.
Guard optional offsets with isset or ??, keep offsets to ASCII data, and route user-facing text through multibyte-aware functions.
Set a rule that byte-level string access is for protocol and identifier data only, so character handling of user text is consistently multibyte-aware.
## Strings as byte arrays Internally a PHP string is a sequence of **bytes** with a length. Square-bracket indexing exposes those bytes: - `$s[0]` is the first byte, `$s[1]` the second, and so on. - The result is always a **string** of length 1, not an integer code. - **Negative offsets** count from the end: `$s[-1]` is the last byte, `$s[-2]` the one before. For an appointment reference like `'APT-2291'`, `$ref[0]` is `'A'` and `$ref[-1]` is `'1'`, handy for a quick prefix or check-digit test. The same access works inside a double-quoted string with simple interpolation: `"Ends in $ref[-1]"` prints `Ends in 1`. ## Reading out of range | Expression (`$ref = 'APT-2291'`) | Result | Diagnostic | |---|---|---| | `$ref[0]` | `'A'` | none | | `$ref[-1]` | `'1'` | none | | `$ref[8]` | `''` | `E_WARNING` Uninitialized string offset 8 | | `$ref[-9]` | `''` | `E_WARNING` Uninitialized string offset -9 | | `isset($ref[8])` | `false` | none | | `$ref[8] ?? '?'` | `'?'` | none | The warning-and-empty-string behaviour means an off-by-one does not stop the script; it silently produces `''`. Use `isset()` or `??` when an offset may legitimately be missing. ## Writing single bytes String offsets are writable, with a few rules: 1. `$s[0] = 'J';` replaces exactly one byte. 2. Writing **past the end** extends the string and pads the gap with spaces: after `$s = 'ab'; $s[4] = 'z';`, `$s` is `'ab z'`. 3. Assigning a **longer** string, `$s[0] = 'Xy'`, stores only `'X'` and warns "Only the first byte will be assigned to the string offset". 4. Assigning an **empty** string throws an `Error`: "Cannot assign an empty string to a string offset". 5. A negative offset beyond the start, such as `$s[-10] = 'x'` on a short string, warns "Illegal string offset" and leaves the string unchanged. 6. Increment and decrement on an offset (`$s[0]++`) throw an `Error`: "Cannot increment/decrement string offsets". For anything longer than one byte, `substr()` and `substr_replace()` are the clearer tools. ## Offset types Offsets must be integers or integer-like strings. A float is truncated to an integer with a "String offset cast occurred" warning. A string that is not numeric, such as `$s['x']`, throws a `TypeError` in PHP 8; a leading-numeric string such as `'1x'` uses its leading integer with a warning. ## Removed and changed syntax - `$s{0}` with braces was deprecated in PHP 7.4 and **removed in PHP 8.0**; it is now a parse error. Legacy code using it must switch to `$s[0]`. - Negative offsets arrived in PHP 7.1; before that they warned and returned `''`. ## Where offsets are the right tool Byte offsets are fast and simple, and they fit data that is ASCII by design: - **Prefix checks on codes.** `$ref[0] === 'A'` for an appointment reference, although `str_starts_with()` reads better for longer prefixes. - **Check digits.** Reading `$ref[-1]` and comparing it with a computed value. - **Masking.** Replacing single bytes of an ASCII token before logging it, one offset at a time. - **Parsing fixed-width records**, where each field starts at a known byte position. For user-supplied text, names, messages and anything that may contain accents or other non-ASCII characters, byte offsets are the wrong tool. ## Bytes, not characters Because offsets address bytes, they are only character-safe for single-byte text. In UTF-8, `'Émile'` starts with the two-byte sequence for `É`, so `$name[0]` returns only its first byte, an invalid fragment that prints as garbage. For user-facing text, such as the first initial in "Dear É. Martin", character-aware functions from the multibyte extension are needed; offsets remain fine for ASCII identifiers like booking codes.
- How do you read a string offset that may not exist without a warning?Check it with `isset($s[$i])`, or use the null-coalescing form `$s[$i] ?? ''`. Both use the quiet lookup mode, which returns null for a missing offset instead of warning, so `??` then supplies the default.
- Why does $name[0] print garbage for a name like 'Émile'?Offsets address bytes. In UTF-8, `É` is two bytes, so `$name[0]` returns only the first byte, which is not a valid character on its own. Offsets are safe for ASCII codes; for user text, use character-aware multibyte functions.
String offsets are like numbered seats in a row counted from either end: seat 0 is the first, seat -1 the last. Asking for a seat beyond the row gets you an empty seat and a complaint, not a crash.
saying these in an interview costs you the question
- $s[0] returns the character's integer code
- Reading past the end of a string throws an exception
- $s[-1] is invalid because offsets cannot be negative
- $s{0} still works in PHP 8 as an alias
- String offsets count characters, so they are safe for UTF-8