PHP RFC: array_partition()
- Version: 0.1
- Date: 2026-10-25
- Author: Ayoub Ezzini, [email protected]
- Status: Under Discussion
- Implementation: https://github.com/php/php-src/pull/24568
- Discussion thread: https://news-web.php.net/php.internals/132923
- Voting thread: tbd
Introduction
Splitting an array into the elements that satisfy a condition and the elements that do not is a common operation. PHP has no built-in function for it, so code today does one of the following:
// Two passes, with the condition duplicated and negated $adults = array_filter($users, fn($u) => $u->age >= 18); $minors = array_filter($users, fn($u) => $u->age < 18); // One filter plus a set difference on the keys $adults = array_filter($users, fn($u) => $u->age >= 18); $minors = array_diff_key($users, $adults); // A hand-written loop $adults = []; $minors = []; foreach ($users as $key => $user) { if ($user->age >= 18) { $adults[$key] = $user; } else { $minors[$key] = $user; } }
The first form repeats the condition and calls the callback twice per element. The second relies on a less obvious idiom. The third is correct but verbose for a simple intent.
This RFC proposes a single function that states the intent directly:
[$adults, $minors] = array_partition($users, fn($u) => $u->age >= 18);
Proposal
Add the following function to ext/standard. Its behaviour is that of this userland implementation:
function array_partition(array $array, callable $callback): array { $matching = []; $nonMatching = []; foreach ($array as $key => $value) { if ($callback($value, $key)) { $matching[$key] = $value; } else { $nonMatching[$key] = $value; } } return [$matching, $nonMatching]; }
The only observable difference is that the native function keeps by-reference elements of the input as references, like array_filter() (see “By-reference elements” below and “Open Issues”).
The callback is called once for each element, in array order, and receives the value and the key, like array_find(), array_find_key(), array_any() and array_all(). The function returns a list of exactly two arrays:
- index
0: the elements for which the callback returned a truthy value; - index
1: all other elements.
Keys are preserved in both arrays, and the relative order of the elements is kept. The input array is not modified.
Examples
var_dump(array_partition([1, 2, 3, 4, 5], fn($v) => $v % 2 === 1)); // [[0 => 1, 2 => 3, 4 => 5], [1 => 2, 3 => 4]] [$named, $indexed] = array_partition( ['a' => 1, 'b' => 2, 5 => 3, 6 => 4], fn($value, $key) => is_string($key), ); // $named = ['a' => 1, 'b' => 2] // $indexed = [5 => 3, 6 => 4] var_dump(array_partition([], fn($v) => true)); // [[], []]
Semantics in detail
- The return value of the callback is interpreted with the usual boolean conversion, as in
array_filter(). array_partition($a, $cb)[0]is identical (===) toarray_filter($a, $cb, ARRAY_FILTER_USE_BOTH), andarray_partition($a, $cb)[1]is identical toarray_diff_key($a, array_partition($a, $cb)[0]).- An empty input returns
[[], []], so the result can always be destructured into two variables. - If the callback throws, the exception is propagated, no further elements are visited and no partial result is returned.
- Elements that are references in the input stay references in the result, as with
array_filter(). See “By-reference elements” below. - There is no
$modeparameter. The callback always receives both the value and the key, followingarray_find()rather thanarray_filter(). A callback that accepts only the value works unchanged, because userland functions ignore extra arguments.
Like array_find(), passing an internal function with a fixed arity as the callback fails, because it receives two arguments:
array_partition(['x'], 'is_string'); // ArgumentCountError: is_string() expects exactly 1 argument, 2 given array_partition(['x'], is_string(...)); // same error array_partition(['x'], fn($v) => is_string($v)); // works array_partition(['x'], is_string(?)); // works, partial function application (PHP 8.6+)
By-reference elements
An element that is a reference in the input, and is still referenced elsewhere, is a reference in the result too. Writing to it through the result changes the input, and the other way round:
$input = [1, 2, 3]; $ref = &$input[0]; [$matching, $rest] = array_partition($input, fn($v) => $v === 1); $matching[0] = 'changed'; var_dump($input[0]); // string(7) "changed" $ref = 'again'; var_dump($matching[0]); // string(5) "again"
A reference that is no longer shared, for example after unset($ref), is copied as a plain value:
$input = [1, 2, 3]; $ref = &$input[0]; unset($ref); [$matching, $rest] = array_partition($input, fn($v) => $v === 1); $matching[0] = 'changed'; var_dump($input[0]); // int(1)
This is the same behaviour as array_filter(), array_diff_key(), array_slice(), array_values() and array_chunk(). Whether array_partition() should keep it is listed under “Open Issues”.
Why the matching elements come first
Every prior-art implementation listed below returns the matching elements first. That order also reads naturally in the common destructuring form [$matching, $rest] = array_partition(...).
A result keyed by true and false was suggested during the discussion. PHP casts boolean array keys to integers (true becomes 1 and false becomes 0), so such a result would have to put the non-matching elements at index 0. Whichever layout is chosen, one of the two idioms silently returns the wrong bucket. This RFC keeps the order used by other languages. Note that, with the proposed layout, the following does not work as it reads:
// Wrong: true is cast to 1, so $passed receives the NON-matching elements [true => $passed, false => $rejected] = array_partition($values, $callback); // Correct [$passed, $rejected] = array_partition($values, $callback);
Why not a general grouping function?
A function that groups elements by an arbitrary key returned by the callback is more general, and is already the subject of a separate RFC (see “Related RFCs”). It does not replace a two-way split:
- The buckets are only created when at least one element falls into them, so destructuring needs a fallback (
$groups[true] ?? []) for each side. Defaults in destructuring itself were proposed by the Destructuring Coalesce RFC, which was declined. - Boolean callback results would have to be used as keys, which leads to the casting problem described above.
- Declaring the groups up front, for example with an extra parameter listing the keys to create, solves the missing buckets but not the casting problem. List destructuring matches by key, not by position, so seeding
[true, false]still produces the keys1and0, and[$matching, $rest] = ...receives the buckets swapped. String labels avoid this (fn($u) => $u->age >= 18 ? 'adults' : 'minors' with['adults', 'minors']). However, the callback, the list of keys and the destructuring keys then have to agree, and a typo silently creates an extra group. - The languages listed below that have both concepts provide them as separate functions (for example Kotlin
partitionandgroupBy, Rustpartitionand the itertoolsgroup_by, LaravelpartitionandgroupBy).
PHP already provides simpler, specialised functions alongside general ones, such as array_find() and array_any() alongside array_filter(). The two proposals are complementary, and this RFC does not block a future grouping function.
Parameter naming
The parameters are named $array and $callback, as for the other callback-taking array functions (array_filter(), array_map(), array_walk(), array_find(), array_find_key(), array_any(), array_all()). $predicate was suggested as a more descriptive name. It was not chosen, so that the callback: named argument works the same way across the whole family.
Prior art
- Laravel:
Illuminate\Support\Collection::partition()returns two collections, passing first. The callback receives the value and the key, and keys are preserved. - Ruby:
Enumerable#partitionreturns[true_elements, false_elements]. - Kotlin:
Iterable<T>.partition()returns aPairwhosefirstholds the elements for which the predicate is true. - Rust:
Iterator::partition()returns a tuple(matching, non_matching). - Haskell:
Data.List.partitionreturns a pair(matching, non_matching). - JavaScript: an
Array.prototype.partitionproposal was published in 2020 (https://github.com/joaolucasl/proposal-array-prototype-partition, discussion at https://es.discourse.group/t/array-prototype-partition-like-filter-but-two-arrays-for-true-and-false/447) but did not advance. ECMAScript 2024 added the generalObject.groupBy()andMap.groupBy()instead. Lodash provides_.partition().
Related RFCs
- Introduce array_group and array_group_pair grouping functions (2023, Draft, never voted). It proposes general grouping, which is complementary to this RFC (see “Why not a general grouping function?”).
- array_find (PHP 8.4). This RFC follows its callback signature.
- Destructuring Coalesce (2022, Declined). It would have allowed defaults in destructuring, which a grouping-based approach needs for missing buckets.
Backward Incompatible Changes
Adding a new function is not considered a backward incompatible change, but the name array_partition becomes reserved in the global namespace. Code that declares its own global array_partition() without a function_exists() guard fails with “Cannot redeclare function array_partition()”. Code with such a guard no longer declares its own version and silently uses the native function instead, which may behave differently. In both cases the code needs to be adjusted: renamed, namespaced, or checked against the native semantics.
Existing userland functions with this name often use a different meaning: they split an array into N chunks.
- The
array-partitionWordPress plugin by coffee2code defined a globalarray_partition($array, $number_of_columns)in version 1.0, guarded byfunction_exists()from 1.0.2. It was renamed toc2c_array_partition()in 1.1 and the deprecated name was removed in 1.3 (2020-09-10). Sites running a version older than 1.3 would no longer fatal on declaration because of the guard. However, calls such asarray_partition($posts, 3)would reach the native function and throw aTypeError. - php-src issue #14582 (June 2024) requested an
array_partition()that splits an array into a given number of parts, likearray_chunk()but by count. It was closed by its author, who noted thatarray_chunk($array, (int) ceil(count($array) / $n))covers the use case.
The name was kept because “partition” by predicate is the established meaning across the languages listed under “Prior art”, and because the N-chunks operation is already available through array_chunk(). The documentation should state the difference explicitly.
Proposed PHP Version(s)
Next minor version (PHP 8.7).
RFC Impact
To the Ecosystem
IDEs, language servers and static analysers need to add the new function to their stubs. A precise return type can be expressed as array{0: array<K, V>, 1: array<K, V>} for an input of type array<K, V>. Polyfills are straightforward, because the behaviour can be reproduced with array_filter() and array_diff_key(), or with a foreach loop.
To Existing Extensions
None. Only ext/standard gains a function. array_filter() and all other existing array functions are unchanged.
To SAPIs
None.
Performance
The value of this function is clarity, not speed: the cost is dominated by invoking the callback. Measured on a release build (./configure --disable-all, php -n), Apple M3 Pro under Docker (aarch64), callback fn($v) => $v % 2 === 0, median of 21 runs over 3 rounds, 1,000,000 elements:
| Approach | Packed int list | String keys |
|---|---|---|
array_partition() | 40.2 - 42.2 ms | 42.1 - 45.3 ms |
array_filter() + array_diff_key() | 39.8 - 40.4 ms | 42.8 - 44.6 ms |
array_filter() twice | 88.5 ms | 90.3 ms |
foreach loop | 34.8 - 35.7 ms | 40.4 - 45.4 ms |
array_partition() is on par with array_filter() + array_diff_key() (within about 2%), about 2.2 times faster than filtering twice, and about 5 - 12% slower than a hand-written loop, which avoids the function call per element. The benchmark script and the raw output are available on request.
Open Issues
- Reference handling. The proposed implementation keeps references, as described in “By-reference elements”. This matches the array functions that return part of their input (
array_filter(),array_diff_key(),array_slice(),array_values(),array_chunk()), and means that replacing thearray_filter()+array_diff_key()idiom witharray_partition()does not change behaviour. On the other hand, the behaviour cannot be reproduced exactly in userland, and writing to a result element can modify the input.array_find()drops references, but it returns a single value rather than an array. Dropping references instead would be a one-line change in the implementation. Feedback on which behaviour is preferred is welcome.
Future Scope
- Support for
iterableor generators (for example accepting aTraversableand returning two arrays). This RFC follows the otherarray_*functions and only accepts arrays. - A general grouping function, as proposed by the array_group RFC.
Voting Choices
Primary Vote requiring a 2/3 majority to accept the RFC:
Patches and Tests
Implementation
After the RFC is implemented, this section should contain:
- the version(s) it was merged into
- a link to the git commit(s)
- a link to the PHP manual entry for the feature
References
- Discussion on internals: https://news-web.php.net/php.internals/132923
- array_find RFC: https://wiki.php.net/rfc/array_find
- array_group RFC: https://wiki.php.net/rfc/array_group
Rejected Features
- A
$modeparameter mirroringarray_filter()(ARRAY_FILTER_USE_KEY/ARRAY_FILTER_USE_BOTH): removed after review in favour of thearray_find()signature. - A result keyed by
true/false: see “Why the matching elements come first”. - Naming the callback parameter
$predicate: see “Parameter naming”.
Changelog
- 0.1 (2026-10-25): Initial draft.