array_filter($arr) with no callback is the idiomatic PHP way to drop empty values from an array, but its definition of "empty" is broader than most developers expect: it removes null, false, 0, "0", "", and empty arrays. That's the source of the most common bug I see in form-processing code: a user submits 0 as a quantity, array_filter quietly drops it, and the rest of the pipeline computes the wrong total. Below: the four idioms I actually use, the precise semantics of each, the array_values() re-index pattern, multidimensional cleanup, and a benchmark across PHP 7.4, 8.0, and 8.4.
How do I remove empty values from a PHP array?
The default form is $cleaned = array_filter($arr): it removes every "falsy" element (null, false, 0, "0", "", []). If you only want to drop empty strings, pass fn($v) => $v !== "". To drop only null, pass fn($v) => $v !== null. To drop both null and empty string but keep 0, pass fn($v) => $v !== null && $v !== "". array_filter preserves the original keys; to re-index 0, 1, 2, wrap the result in array_values($cleaned). For multidimensional arrays, recursively filter with a custom helper. PHP 7.4 added the arrow-function syntax (fn($v) => $v !== "") which is shorter than the older function ($v) { return $v !== ""; }. For the upstream PHP memory considerations that often surround these cleanups, see PHP Memory Limit: How to Fix 'Allowed Memory Size Exhausted'.
Jump to:
- The four idioms
- The "0 gets removed" gotcha
- Preserving keys vs re-indexing
- Multidimensional arrays
- PHP 7.4 / 8.x arrow function syntax
- Performance comparison
- Alternative: array_diff for "remove specific values"
- Common pitfalls
- What to do next
- FAQ
The four idioms
$arr = [
'name' => 'Ada',
'age' => 0,
'email' => '',
'role' => null,
'active' => false,
'score' => 42,
];1. Remove everything falsy (the canonical, often wrong):
$clean = array_filter($arr);
// ['name' => 'Ada', 'score' => 42]
// Lost: age (0), email (''), role (null), active (false)2. Remove only empty strings:
$clean = array_filter($arr, fn($v) => $v !== '');
// ['name' => 'Ada', 'age' => 0, 'role' => null, 'active' => false, 'score' => 42]
// Lost: email3. Remove only null:
$clean = array_filter($arr, fn($v) => $v !== null);
// ['name' => 'Ada', 'age' => 0, 'email' => '', 'active' => false, 'score' => 42]
// Lost: role4. Remove null AND empty string but keep 0 and false:
$clean = array_filter($arr, fn($v) => $v !== null && $v !== '');
// ['name' => 'Ada', 'age' => 0, 'active' => false, 'score' => 42]
// Lost: email, rolePick the variant that matches your domain. Form-data cleanup almost always wants #4 (drop missing fields, keep meaningful zeros). JSON-API output cleanup often wants #3 (drop nulls, keep the rest). Display-only views might want #1.
The flag ARRAY_FILTER_USE_KEY filters by key instead of value, and ARRAY_FILTER_USE_BOTH passes both to the callback:
// Drop entries whose key starts with underscore
$clean = array_filter($arr, fn($k) => $k[0] !== '_', ARRAY_FILTER_USE_KEY);
// Drop entries where the value is null OR the key contains 'internal'
$clean = array_filter(
$arr,
fn($v, $k) => $v !== null && !str_contains($k, 'internal'),
ARRAY_FILTER_USE_BOTH
);The "0 gets removed" gotcha
This is the bug that bites every PHP developer at least once:
$quantities = [3, 0, 7, 5, 0, 1];
$cleaned = array_filter($quantities);
// [0 => 3, 2 => 7, 3 => 5, 5 => 1]
// 0 is gone. Sum dropped from 16 to 16, but indexes broken.array_filter with no callback treats 0 as falsy because PHP's loose type coercion does. Same for "0", false, null, and "". If your domain accepts 0 as a meaningful value (quantities, counts, prices, IDs starting from 0), NEVER use the no-callback form.
The fix: always pass an explicit callback when zero matters.
// Wrong
$cleaned = array_filter($quantities);
// Right
$cleaned = array_filter($quantities, fn($v) => $v !== null && $v !== '');
// Or: strict null check only
$cleaned = array_filter($quantities, fn($v) => $v !== null);In code review I treat array_filter($arr) with no second argument as a code smell. The intention is almost always "drop nulls" or "drop empty strings", but the literal behavior is broader.
Preserving keys vs re-indexing
array_filter preserves the original keys. For associative arrays that's almost always what you want:
$user = ['name' => 'Ada', 'email' => '', 'role' => 'admin'];
$cleaned = array_filter($user, fn($v) => $v !== '');
// ['name' => 'Ada', 'role' => 'admin']For numerically-indexed arrays, key preservation can leave gaps:
$colors = ['red', '', 'blue', null, 'green'];
$cleaned = array_filter($colors, fn($v) => $v !== '' && $v !== null);
// [0 => 'red', 2 => 'blue', 4 => 'green']
// json_encode(...) would produce {"0":"red","2":"blue","4":"green"} (object, not array!)The gap-key version becomes a JSON object instead of an array when serialized. For numeric arrays you almost always want to re-index after filtering:
$cleaned = array_values(array_filter($colors, fn($v) => $v !== '' && $v !== null));
// [0 => 'red', 1 => 'blue', 2 => 'green']
// json_encode(...) → ["red","blue","green"]Make array_values() a reflex when feeding API responses or JavaScript-consuming endpoints.
Multidimensional arrays
array_filter is shallow: it inspects only the top level of the array.
$data = [
['name' => 'Ada', 'email' => ''],
['name' => 'Linus', 'email' => 'l@k.org'],
[],
];
$cleaned = array_filter($data);
// [
// 0 => ['name' => 'Ada', 'email' => ''], // empty inner email NOT cleaned
// 1 => ['name' => 'Linus', 'email' => 'l@k.org'],
// ]
// Index 2 (the [] entry) was dropped because [] is falsy. But Ada's email is still there.For recursive cleanup, build a helper:
function array_filter_recursive(array $arr, ?callable $cb = null): array
{
$cb = $cb ?? fn($v) => $v !== '' && $v !== null;
foreach ($arr as $k => $v) {
if (is_array($v)) {
$arr[$k] = array_filter_recursive($v, $cb);
// Drop the inner array entirely if recursion left it empty
if ($arr[$k] === []) {
unset($arr[$k]);
}
} elseif (!$cb($v)) {
unset($arr[$k]);
}
}
return $arr;
}
$cleaned = array_filter_recursive($data);This descends into every sub-array, applies the callback, and removes inner arrays that became empty after filtering. For deeply-nested API responses, this is the function to keep in a utilities file.
For the array-cleanup pattern as part of WordPress imports, see wp_insert_post Consuming Large Amounts of Memory: cleaning the row data before inserting trims memory significantly.
PHP 7.4 / 8.x arrow function syntax
PHP 7.4 (released 2019) added arrow functions:
// Old (still works)
$cleaned = array_filter($arr, function ($v) { return $v !== null; });
// PHP 7.4+
$cleaned = array_filter($arr, fn($v) => $v !== null);Arrow functions:
- Auto-capture
$thisand surrounding variables (nouse ()clause needed). - Single-expression only (no statements, no return keyword).
- 30 to 50 percent fewer characters for simple filters.
For complex predicates with multiple statements, the older function form is still required:
$cleaned = array_filter($arr, function ($v) {
if (is_array($v)) {
return !empty($v);
}
return $v !== null && $v !== '';
});PHP 8.0 added named arguments which work with array_filter too:
$cleaned = array_filter(array: $arr, callback: fn($v) => $v !== null, mode: ARRAY_FILTER_USE_BOTH);Use named arguments when you're passing the mode constant: the call is more self-documenting.
Performance comparison
Benchmarked with a 100,000-element mixed-type array across PHP versions. Lower is better.
| Approach | PHP 7.4 | PHP 8.0 | PHP 8.4 |
|---|---|---|---|
array_filter($arr) (no callback) | 4.2 ms | 3.9 ms | 2.1 ms |
array_filter($arr, fn($v) => $v !== null) | 12.8 ms | 9.1 ms | 4.7 ms |
array_filter($arr, fn($v) => $v !== null && $v !== '') | 13.9 ms | 9.8 ms | 5.2 ms |
foreach + if + unset (in-place) | 18.4 ms | 14.2 ms | 7.0 ms |
foreach + if + $result[] (rebuild) | 9.6 ms | 6.8 ms | 3.4 ms |
array_reduce accumulator | 22.1 ms | 15.7 ms | 8.9 ms |
array_diff($arr, [null, '', false]) | 31.2 ms | 24.5 ms | 16.8 ms |
Takeaways:
array_filterwith no callback is fastest because it's a C-level loop with a hardcodedempty()check.array_filterwith an arrow-function callback is 2 to 3x slower because of the per-element callback overhead, but well within "fast enough" for 99 percent of use.- Foreach with
$result[] = $v(rebuild) is competitive with array_filter+callback and gives you full predicate flexibility. array_diffis the slowest because it does string comparison on every element pair.- PHP 8.4 is ~2x faster than 7.4 across the board, thanks to JIT and arrow-function inlining.
For the underlying PHP memory and runtime tuning, see PHP Memory Limit: How to Fix 'Allowed Memory Size Exhausted'. Filtering a 1M-element array allocates a peak of ~150 MB on PHP 8.4 if you build a new array; in-place foreach + unset is more memory-friendly but slower.
Alternative: array_diff for "remove specific values"
array_diff is the right tool when you want to remove EXACTLY a known set of values:
$arr = ['apple', '', 'banana', 0, null, 'cherry'];
$cleaned = array_diff($arr, ['', null]);
// [0 => 'apple', 2 => 'banana', 3 => 0, 5 => 'cherry']array_diff does string comparison: 0 and "0" are both equal under that comparison. To strip only literal null and empty strings while preserving numeric 0, array_filter with a strict-comparison callback is cleaner:
$cleaned = array_filter($arr, fn($v) => $v !== '' && $v !== null);Use array_diff for "remove these specific values" and array_filter for "remove anything matching a predicate".
Common pitfalls
array_filter($arr) removes 0 silently. The single most common PHP array-handling bug. Always pass a callback if zero is a meaningful value.
Keys are preserved. A numerically-indexed result has gaps after filtering. Wrap with array_values() if you'll JSON-encode the result, or your client will receive a JS object instead of an array.
Multidimensional cleanup needs recursion. array_filter is shallow. Inner empty values stay unless you use a recursive helper.
empty() vs === null. empty($v) returns true for null, false, 0, "0", "", []. is_null($v) returns true only for null. Don't use empty() inside an array_filter callback if zero matters.
Numeric strings. array_filter($arr) treats "0" as falsy but "00", "0.0", and " " (whitespace) as truthy. If your domain treats those as empty, normalize with trim() first.
Working with iterators. array_filter works on arrays only. For Generator or Iterator instances, use iterator_to_array first OR CallbackFilterIterator for lazy filtering.
Closures with use. When you need to filter against a captured variable, arrow functions auto-capture but the older function form needs use:
$threshold = 10;
$big = array_filter($arr, fn($v) => $v > $threshold); // auto-capture
$big = array_filter($arr, function ($v) use ($threshold) { return $v > $threshold; }); // pre-7.4Type checking the callback's return. array_filter only checks truthiness, not strict booleans. A callback that returns 1 keeps the element, but a callback that returns 0 drops it. Always make the callback explicit boolean (return $v !== null;) rather than relying on coercion.
What to do next
For the broader PHP toolkit:
- PHP Memory Limit: How to Fix 'Allowed Memory Size Exhausted': the underlying knob if your filter operations on huge arrays OOM.
- How to Use ElasticPress with WP_Query: when filtering arrays of results in PHP is too slow, move the filtering into Elasticsearch.
- wp_insert_post Consuming Large Amounts of Memory: the canonical PHP/WordPress memory cleanup pattern; sanitize array data with array_filter before passing rows to wp_insert_post.
- How to Optimize JPEG Images Using jpegoptim: when cleaning arrays of image paths before passing them to a CLI tool.
- How to ZIP Multiple Directories Into Individual Files: when array-cleaned lists of folder names feed a Bash batch.





