skip to content

String Functions & Formatting

The standard string functions search, slice, split and format bytes: str_contains, strpos with its 0-versus-false trap, explode, str_replace and sprintf. Interviewers use them to test fluency.

part ofPHPoverview, primer and where to startread it →
on this pageshow

explore

questions

6

In PHP, how do explode(), implode() and trim() behave when you split a comma-separated list of invoice codes, and which edge cases bite?

level: juniorimportance: must knowfreq 62%

answer

  1. separator is the first argument
  2. limit keeps the rest in the last element
  3. empty input gives one empty element
  4. empty separator throws ValueError
  5. trim takes a character list

basics

~20 s

explode(',', $s) splits on a separator and returns an array, implode(',', $parts) joins it back, and trim() strips whitespace. Traps: an empty string explodes to [''], an empty separator throws ValueError, and trim's second argument is a set of characters.

solid answer

~40 s

`explode(string $separator, string $string, int $limit = PHP_INT_MAX): array` splits on every occurrence of the separator. A positive `$limit` caps the number of elements and leaves the remainder in the last one; a negative limit drops that many elements from the end. Exploding an empty string returns `['']`, one empty element, so `count()` is 1, and since PHP 8.0 an empty separator throws `ValueError`. `implode(', ', $parts)` joins with the separator first; the old reversed argument order was removed in 8.0. `trim()` strips spaces, tabs, newlines, carriage returns, vertical tabs and NUL bytes from both ends, and its optional second argument is a **list of characters**, not a substring, so `rtrim('1200', '0')` returns `'12'`. A robust split maps `trim` over the parts and drops the empty ones.

code

php · 15 lines
php
<?php
declare(strict_types=1);

$raw = " INV-001, INV-002 ,,INV-003\n";

$codes = array_map(trim(...), explode(',', $raw));
$codes = array_values(array_filter($codes, fn (string $c): bool => $c !== ''));

echo implode(';', $codes), "\n";      // INV-001;INV-002;INV-003

var_dump(explode(',', ''));            // array(1) { [0]=> string(0) "" }
var_dump(explode(',', 'a,b,c', 2));    // ['a', 'b,c']
var_dump(explode(',', 'a,b,c', -1));   // ['a', 'b']
var_dump(trim('INV-001-INV', 'INV'));  // string(5) "-001-"
var_dump(rtrim('1200', '0'));          // string(2) "12"

go deeper

for a junior

Recall the argument order of explode and implode, the default characters trim removes, and that trimming each part is part of a clean split.

for a middle

Explain the positive, negative and zero limit rules, the [''] result for an empty string, the ValueError for an empty separator and trim's character-set argument.

for a senior

Show you would harden an import against empty parts, pasted Unicode whitespace and '0' values that a callback-less array_filter drops, and switch to a CSV parser when fields can contain the separator.

for a principal

Argue when ad hoc explode parsing is acceptable versus when a documented format and a real parser should be mandated for data exchanged with other systems.

## Splitting with explode() `explode(string $separator, string $string, int $limit = PHP_INT_MAX): array` cuts `$string` at every occurrence of `$separator` and returns the pieces as a list. The separator comes **first** and the string second; swapping them is a common slip. The `$limit` argument changes the shape of the result: | Call | Result | Rule | |---|---|---| | `explode(',', 'a,b,c,d')` | `['a', 'b', 'c', 'd']` | split everywhere | | `explode(',', 'a,b,c,d', 2)` | `['a', 'b,c,d']` | at most 2 elements; the last holds the rest | | `explode(',', 'a,b,c,d', -1)` | `['a', 'b', 'c']` | drop the last element | | `explode(',', 'a,b,c,d', 0)` | `['a,b,c,d']` | 0 is treated as 1 | | `explode(',', 'abcd')` | `['abcd']` | no separator: one element | | `explode(',', '')` | `['']` | empty input: one empty element | A positive limit is useful for `key=value` lines where the value may itself contain the separator: `explode('=', $line, 2)` always yields at most two parts. ## Edge cases that bite 1. **Empty input is not an empty array.** `explode(',', '')` returns an array with one empty string, so `count()` is `1`. Code that asks "were any invoice codes supplied?" with `count(explode(...)) > 0` always says yes. 2. **Empty separator.** Since PHP 8.0 `explode('', $s)` throws a `ValueError`; before, it returned `false` with a warning. Splitting into single bytes is `str_split()`'s job. 3. **Adjacent separators.** `'a,,b'` produces an empty element in the middle, and a trailing comma produces one at the end. 4. **Whitespace survives.** `' INV-001, INV-002 '` explodes into elements that still carry their spaces. ## Cleaning the parts with trim() `trim(string $string, string $characters = " \n\r\t\v\0"): string` removes characters from both ends. The default set is: - the space `" "` - newline `\n` and carriage return `\r` - horizontal tab `\t` and vertical tab `\v` - the NUL byte `\0` `ltrim()` works only on the left, `rtrim()` only on the right, and `chop()` is an alias of `rtrim()`. The second argument is a **set of characters**, not a substring to remove. Any character in the set is stripped, in any order, until a character outside the set is reached: - `trim('INV-001-INV', 'INV')` returns `'-001-'`, because `I`, `N` and `V` are each stripped from both ends. - `rtrim('1200', '0')` returns `'12'`, which silently corrupts an amount if you meant to drop only formatting zeros. - A range can be written with `..`, as in `trim($binary, "\x00..\x1F")` to strip control bytes. `trim()` works on bytes and knows nothing about Unicode, so a UTF-8 non-breaking space pasted from a spreadsheet is **not** removed by default; the multibyte counterparts handle that. A robust split of a user-typed list therefore combines three steps: 1. `explode(',', $raw)` to cut the string; 2. `array_map(trim(...), ...)` to clean each part; 3. drop the empty strings with a callback that tests `$c !== ''`. Calling `array_filter()` **without** a callback is a trap here: it removes every falsy value, and the string `'0'` is falsy. ## Joining with implode() `implode(string $separator, array $array): string` glues the elements with the separator between them. `join()` is an alias. Details: - `implode(', ', [])` returns `''`. - `implode($array)` with a single argument uses an empty separator. - The legacy reversed order `implode($pieces, $glue)` was deprecated in PHP 7.4 and **removed in 8.0**. - Elements are converted to strings, so `implode(',', [1, 2.5, true])` gives `'1,2.5,1'`. `explode()` and `implode()` are not a quoting format: a code that itself contains the separator cannot round-trip. When fields may contain commas or quotes, use a real CSV parser instead. ## What a strong answer covers Name the argument order, the `$limit` semantics, the `['']` result for an empty string and the `ValueError` for an empty separator. Then show that you clean the pieces with `trim()`, know its default character list, and know that its second argument is a character set. That combination is what separates someone who has debugged an import from someone who has only read the signature.

  • Why does count(explode(',', $input)) > 0 fail as a check that at least one invoice code was supplied?
    Because, without a negative limit, `explode()` does not return an empty array: an empty `$input` yields `['']`, so the count is 1. Check the trimmed input for `''` first, or filter out empty strings after splitting and count what remains.
  • What does the second argument of trim() mean, and why does rtrim('1200', '0') return '12'?
    It is a list of characters to strip, not a substring. `rtrim()` removes any listed character from the right end until it meets one that is not listed, so both zeros go. To drop an exact suffix, test with `str_ends_with()` and cut with `substr()` instead.
  • What happens in PHP 8 when explode() receives an empty separator?
    It throws a `ValueError`. Before PHP 8.0 it emitted a warning and returned `false`. If the goal is to split a string into single bytes, `str_split()` is the function for that; for characters in UTF-8 text use the multibyte variant.

saying these in an interview costs you the question

  • explode returns an empty array when the input string is empty.
  • trim's second argument removes that exact substring from the ends.
  • trim also strips non-breaking spaces and other Unicode whitespace by default.
  • implode accepts the array and the separator in either order in PHP 8.
  • explode with an empty separator splits the string into characters.
  • A limit of 2 throws away everything after the second element.
open as a page

In PHP, why is if (strpos($code, 'INV')) a bug, and how should you test whether a string contains a substring?

level: juniorimportance: must knowfreq 80%

basics

~20 s

strpos() returns the byte offset of the first match or false, and a match at offset 0 is falsy, so a plain if treats it as not found. Compare with !== false, or call str_contains(), added in PHP 8.0.

open as a page

In PHP, how do sprintf() conversion specifications like %05d, %'*10s, %.2f and %1$s work when formatting invoice lines?

level: middleimportance: should knowfreq 45%

basics

~10 s

Each sprintf() specification is %[argnum$][flags][width][.precision]specifier: %05d zero-pads an integer to five characters, %'*10s pads a string with asterisks, %.2f prints two decimals and %1$s reuses the first argument. Too few values throw ArgumentCountError.

open as a page

In PHP, why can str_replace() with array arguments give a different result from strtr() given the same replacement pairs?

level: middleimportance: should knowfreq 38%

basics

~20 s

str_replace() applies each search/replace pair in turn over the whole string, so text inserted by one pair can be replaced by a later pair. strtr() with an array makes one pass, tries the longest key first and never re-scans replaced text.

open as a page

A PHP accounting export starts writing 1234,50 instead of 1234.50 after a library calls setlocale(); which formatting functions follow the locale, and how do you make the output locale-proof?

level: seniorimportance: should knowfreq 28%

basics

~20 s

sprintf() and printf() with %f or %g use the LC_NUMERIC decimal point, so a setlocale() call turns 1234.50 into 1234,50. Use %F, or number_format($x, 2, '.', ''), for machine output; echoing a float ignores the locale since PHP 8.0.

open as a page

In PHP, why does sorting invoice codes like INV-9 and INV-10 with strcmp() put INV-10 first, and what does strnatcmp() change?

level: middleimportance: nice to knowfreq 25%

basics

~20 s

strcmp() compares strings byte by byte, so at the first difference '1' sorts before '9' and INV-10 precedes INV-9. strnatcmp() compares runs of digits as numbers, giving the human order INV-9, INV-10; strnatcasecmp() also ignores ASCII case.

open as a page