Advanced Array Functions #

The Array article in the Basics section covered the most frequently used array functions. This article continues from there — covering functions that are less well known but very useful for specific problems: array_multisort for multi-column sorting, array_walk_recursive for transforming nested arrays, the array_u* functions for set operations with custom comparators, and data pipeline patterns that chain several array functions into expressive, readable transformations.

Advanced Sorting #

array_multisort — Multi-column Sorting #

array_multisort lets you sort an array by several criteria at once — like ORDER BY column1 ASC, column2 DESC in SQL:

<?php
$products = [
    ['name' => 'Laptop',  'category' => 'electronics', 'price' => 15000000, 'stock' => 5],
    ['name' => 'Mouse',   'category' => 'accessories', 'price' => 250000,   'stock' => 0],
    ['name' => 'Monitor', 'category' => 'electronics', 'price' => 5000000,  'stock' => 12],
    ['name' => 'Keyboard','category' => 'accessories', 'price' => 800000,   'stock' => 8],
    ['name' => 'Webcam',  'category' => 'electronics', 'price' => 1200000,  'stock' => 3],
];

// Extract the columns to sort by
$category = array_column($products, 'category');
$price    = array_column($products, 'price');

// Sort: category ascending, then price descending within the same category
array_multisort(
    $category, SORT_ASC,  SORT_STRING,
    $price,    SORT_DESC, SORT_NUMERIC,
    $products
);

foreach ($products as $p) {
    echo "{$p['category']} | {$p['name']}: Rp " . number_format($p['price']) . "\n";
}
// accessories | Keyboard: Rp 800.000
// accessories | Mouse: Rp 250.000
// electronics | Laptop: Rp 15.000.000
// electronics | Monitor: Rp 5.000.000
// electronics | Webcam: Rp 1.200.000

// Available sorting flags:
// SORT_REGULAR  — default PHP comparison
// SORT_NUMERIC  — compare as numbers
// SORT_STRING   — compare as strings
// SORT_NATURAL  — natural order (file10 after file9)
// SORT_LOCALE_STRING — compare using the locale
// SORT_FLAG_CASE — case-insensitive (combined with SORT_STRING or SORT_NATURAL)

Multi-criteria usort via Tuples #

A more idiomatic alternative in PHP 8:

<?php
// Using the spaceship operator with array comparison
usort($products, fn($a, $b) =>
    [$a['category'], -$a['price']] <=> [$b['category'], -$b['price']]
);
// Sort: category ascending, price descending (negated to reverse)

// More complex multi-criteria
usort($products, function($a, $b) {
    // 1. Category ascending
    $cmp = strcmp($a['category'], $b['category']);
    if ($cmp !== 0) return $cmp;

    // 2. Stock: available first (stock > 0)
    $aInStock = $a['stock'] > 0 ? 0 : 1;
    $bInStock = $b['stock'] > 0 ? 0 : 1;
    if ($aInStock !== $bInStock) return $aInStock <=> $bInStock;

    // 3. Price ascending
    return $a['price'] <=> $b['price'];
});

array_walk and array_walk_recursive #

array_walk iterates an array and calls a callback for each element, able to modify values by reference. array_walk_recursive does the same but descends into nested arrays:

<?php
// array_walk — in-place modification
$prices = ['laptop' => 15000000, 'mouse' => 250000, 'monitor' => 5000000];

array_walk($prices, function(&$value, $key) {
    $value = 'Rp ' . number_format($value, 0, ',', '.');
});

print_r($prices);
// ['laptop' => 'Rp 15.000.000', 'mouse' => 'Rp 250.000', 'monitor' => 'Rp 5.000.000']

// With additional data (the third argument)
$config = ['host' => 'localhost', 'port' => '3306', 'db' => 'myapp'];
$prefix = 'DB_';

array_walk($config, function(&$value, $key) use ($prefix) {
    putenv("$prefix" . strtoupper($key) . "=$value");
});

// array_walk_recursive — descends into nested arrays
$data = [
    'user' => ['name' => '  Budi  ', 'email' => '  [email protected]  '],
    'tags' => ['  php  ', '  MYSQL  ', '  backend  '],
    'version' => '  2.0  ',
];

array_walk_recursive($data, function(&$value) {
    if (is_string($value)) {
        $value = strtolower(trim($value));
    }
});

print_r($data);
// ['user' => ['name' => 'budi', 'email' => '[email protected]'],
//  'tags' => ['php', 'mysql', 'backend'],
//  'version' => '2.0']

// Transform nested config from YAML/JSON
$config = [
    'database' => [
        'host'     => '${DB_HOST}',
        'password' => '${DB_PASS}',
    ],
    'cache' => [
        'ttl' => '${CACHE_TTL}',
    ],
];

array_walk_recursive($config, function(&$value) {
    if (is_string($value) && preg_match('/^\$\{(.+)\}$/', $value, $m)) {
        $value = getenv($m[1]) ?: $value;
    }
});

Set Operations with Custom Comparators #

PHP provides u (user-defined) versions of the diff and intersect functions — enabling custom comparators for more sophisticated comparisons:

<?php
$productsA = [
    ['id' => 1, 'name' => 'Laptop',  'price' => 15000000],
    ['id' => 2, 'name' => 'Mouse',   'price' => 250000],
    ['id' => 3, 'name' => 'Monitor', 'price' => 5000000],
];

$productsB = [
    ['id' => 2, 'name' => 'Gaming Mouse', 'price' => 350000], // same ID, different name
    ['id' => 4, 'name' => 'Keyboard',     'price' => 800000],
];

// array_udiff — elements in A that aren't in B (with a custom comparator)
$onlyInA = array_udiff($productsA, $productsB, fn($a, $b) => $a['id'] <=> $b['id']);
// Products with IDs not in $productsB: Laptop (id:1) and Monitor (id:3)

// array_uintersect — elements present in both (based on ID)
$same = array_uintersect($productsA, $productsB, fn($a, $b) => $a['id'] <=> $b['id']);
// Mouse (id:2) — present in both

// array_diff_uassoc — diff by key and value (custom comparator for values)
$defaultConfig = ['timeout' => 30, 'retry' => 3, 'debug' => false];
$userConfig    = ['timeout' => 60, 'retry' => 3, 'log' => true];

$different = array_diff_uassoc($defaultConfig, $userConfig, fn($a, $b) => $a <=> $b);
// ['timeout' => 30] — key 'timeout' exists in both but the values differ

// array_udiff_assoc — value diff with a custom comparator, keys must match
$oldProducts = ['laptop' => 15000000, 'mouse' => 250000];
$newProducts = ['laptop' => 14000000, 'mouse' => 250000, 'webcam' => 500000];

$changedPrices = array_udiff_assoc(
    $oldProducts,
    $newProducts,
    fn($a, $b) => $a <=> $b
);
// ['laptop' => 15000000] — the laptop price changed

array_fill_keys and array_fill #

<?php
// array_fill_keys — create an array from a key list with the same default value
$fields = ['name', 'email', 'phone', 'address'];
$emptyForm = array_fill_keys($fields, '');
// ['name' => '', 'email' => '', 'phone' => '', 'address' => '']

// Useful for initializing counters
$categories = ['electronics', 'accessories', 'furniture'];
$counter    = array_fill_keys($categories, 0);
// ['electronics' => 0, 'accessories' => 0, 'furniture' => 0]

foreach ($products as $p) {
    $counter[$p['category']]++;
}

// Merge with existing data — fill defaults for missing keys
$userData     = ['name' => 'Budi', 'email' => '[email protected]'];
$completeData = array_merge($emptyForm, $userData);
// ['name' => 'Budi', 'email' => '[email protected]', 'phone' => '', 'address' => '']

// array_fill — create an array with the same value N times
$defaultPermissions = array_fill(0, 5, false); // [false, false, false, false, false]
$slots   = array_fill(1, 7, null);             // {1: null, 2: null, ..., 7: null}

// Combined to build a weekly schedule
$days    = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'];
$schedule = array_fill_keys($days, []);
// ['Monday' => [], 'Tuesday' => [], ..., 'Sunday' => []]

array_splice — Full Operations #

array_splice is a versatile function that can remove, replace, and insert at the same time:

<?php
$fruits = ['apple', 'mango', 'orange', 'grape', 'watermelon'];

// Remove 2 elements starting at index 1
$removed = array_splice($fruits, 1, 2);
// $fruits: ['apple', 'grape', 'watermelon']
// $removed: ['mango', 'orange']

// Insert without removing (length = 0)
array_splice($fruits, 2, 0, ['kiwi', 'lemon']);
// $fruits: ['apple', 'grape', 'kiwi', 'lemon', 'watermelon']

// Replace 1 element with 2 new elements
array_splice($fruits, 1, 1, ['fragrant mango', 'arum mango']);
// $fruits: ['apple', 'fragrant mango', 'arum mango', 'kiwi', 'lemon', 'watermelon']

// Remove from the end (negative index)
array_splice($fruits, -2); // remove the last 2 elements

// Comparison: array_splice vs unset
$arr = ['a', 'b', 'c', 'd'];

unset($arr[1]);
// ['a', 'c', 'd'] — sparse indexes: [0]=>'a', [2]=>'c', [3]=>'d'

$arr = ['a', 'b', 'c', 'd'];
array_splice($arr, 1, 1);
// ['a', 'c', 'd'] — reindexed: [0]=>'a', [1]=>'c', [2]=>'d'

array_pad — Pad to a Specific Size #

<?php
$data = [1, 2, 3];

// Pad to the right (positive) — add at the end
$padded = array_pad($data, 5, 0);    // [1, 2, 3, 0, 0]
$padded = array_pad($data, 5, null); // [1, 2, 3, null, null]

// Pad to the left (negative) — add at the start
$padded = array_pad($data, -5, 0);   // [0, 0, 1, 2, 3]

// Does nothing if the array is already long enough
$padded = array_pad($data, 2, 0);    // [1, 2, 3] — unchanged

// Useful for ensuring an array has a minimum size
function ensureMinimum(array $arr, int $min, mixed $default = null): array
{
    return count($arr) >= $min ? $arr : array_pad($arr, $min, $default);
}

// Example: show 5 newest products, pad with placeholders if there are fewer
$newestProducts = fetchNewestProducts(limit: 3); // only 3 products
$display        = ensureMinimum($newestProducts, 5, ['placeholder' => true]);
// Now has 5 elements, the last 2 are placeholders

compact and extract #

<?php
// compact() — create an associative array from existing variables
$name  = 'Budi Santoso';
$email = '[email protected]';
$age   = 28;
$role  = 'admin';

$user = compact('name', 'email', 'age', 'role');
// ['name' => 'Budi Santoso', 'email' => '[email protected]', 'age' => 28, 'role' => 'admin']

// Useful for building a data array before saving to the DB or sending to a view
$stmt = $pdo->prepare("INSERT INTO users (name, email, age, role) VALUES (:name, :email, :age, :role)");
$stmt->execute(compact('name', 'email', 'age', 'role'));

// extract() — the reverse of compact: create variables from array keys
$config = ['host' => 'localhost', 'port' => 3306, 'name' => 'mydb'];
extract($config);
// Now there are $host, $port, $name variables in this scope

echo $host; // localhost
echo $port; // 3306

// WARNING: extracting user data is VERY DANGEROUS!
// Never do: extract($_POST) or extract($_GET)
// Attackers can inject arbitrary variables into the scope

// Safe extract with a prefix
extract($config, EXTR_PREFIX_ALL, 'db');
// $db_host, $db_port, $db_name

// Or with EXTR_SKIP — skip if the variable already exists
$host   = 'existing_value'; // won't be overwritten
extract($config, EXTR_SKIP);
echo $host; // still 'existing_value'

Data Pipeline Patterns #

Chaining array functions into expressive data transformations:

<?php
$orders = [
    ['id' => 1, 'status' => 'completed', 'total' => 150000,  'user_id' => 10, 'discount' => 0],
    ['id' => 2, 'status' => 'cancelled', 'total' => 80000,   'user_id' => 12, 'discount' => 0.1],
    ['id' => 3, 'status' => 'completed', 'total' => 2500000, 'user_id' => 10, 'discount' => 0.15],
    ['id' => 4, 'status' => 'completed', 'total' => 300000,  'user_id' => 15, 'discount' => 0],
    ['id' => 5, 'status' => 'processing','total' => 900000,  'user_id' => 12, 'discount' => 0.05],
];

// Pipeline: filter completed → calculate net → group by user → sum totals per user
$revenuePerUser = array_reduce(
    array_map(
        fn($o) => ['user_id' => $o['user_id'], 'net' => $o['total'] * (1 - $o['discount'])],
        array_filter($orders, fn($o) => $o['status'] === 'completed')
    ),
    function($carry, $item) {
        $carry[$item['user_id']] = ($carry[$item['user_id']] ?? 0) + $item['net'];
        return $carry;
    },
    []
);
// [10 => 2277500, 15 => 300000]

// Build a pipeline class for better readability
class ArrayPipeline
{
    private function __construct(private array $data) {}

    public static function from(array $data): static
    {
        return new static($data);
    }

    public function filter(callable $fn): static
    {
        return new static(array_values(array_filter($this->data, $fn)));
    }

    public function map(callable $fn): static
    {
        return new static(array_map($fn, $this->data));
    }

    public function sort(callable $fn): static
    {
        $data = $this->data;
        usort($data, $fn);
        return new static($data);
    }

    public function groupBy(string $key): array
    {
        return array_reduce($this->data, function($carry, $item) use ($key) {
            $carry[$item[$key]][] = $item;
            return $carry;
        }, []);
    }

    public function reduce(callable $fn, mixed $initial = null): mixed
    {
        return array_reduce($this->data, $fn, $initial);
    }

    public function toArray(): array
    {
        return $this->data;
    }

    public function first(): mixed
    {
        return $this->data[0] ?? null;
    }

    public function count(): int
    {
        return count($this->data);
    }
}

// Fluent pipeline usage
$result = ArrayPipeline::from($orders)
    ->filter(fn($o) => $o['status'] === 'completed')
    ->map(fn($o) => [...$o, 'net' => $o['total'] * (1 - $o['discount'])])
    ->sort(fn($a, $b) => $b['net'] <=> $a['net'])
    ->toArray();

$totalRevenue = ArrayPipeline::from($orders)
    ->filter(fn($o) => $o['status'] === 'completed')
    ->reduce(fn($carry, $o) => $carry + $o['total'] * (1 - $o['discount']), 0);

$byStatus = ArrayPipeline::from($orders)->groupBy('status');
// ['completed' => [...], 'cancelled' => [...], 'processing' => [...]]

Other Lesser-Known Array Functions #

<?php
// array_count_values — count the frequency of every value
$values = ['apple', 'mango', 'apple', 'orange', 'mango', 'apple'];
$freq   = array_count_values($values);
// ['apple' => 3, 'mango' => 2, 'orange' => 1]

// Useful for tag clouds and data analysis
$tags     = ['php', 'mysql', 'php', 'redis', 'php', 'mysql'];
$popular  = array_count_values($tags);
arsort($popular); // sort by popularity
// ['php' => 3, 'mysql' => 2, 'redis' => 1]

// array_flip — swap keys and values
$fruits = ['apple' => 'red', 'mango' => 'yellow', 'grape' => 'purple'];
$flip   = array_flip($fruits);
// ['red' => 'apple', 'yellow' => 'mango', 'purple' => 'grape']

// Useful for fast lookups
$statusCodes = [200 => 'OK', 404 => 'Not Found', 500 => 'Error'];
$codeLookup  = array_flip($statusCodes);
echo $codeLookup['Not Found']; // 404

// array_combine — combine two arrays into key => value
$columns = ['id', 'name', 'email'];
$values  = [1, 'Budi', '[email protected]'];
$row     = array_combine($columns, $values);
// ['id' => 1, 'name' => 'Budi', 'email' => '[email protected]']

// Useful when parsing CSV with headers
$handle = fopen('data.csv', 'r');
$header = fgetcsv($handle);
while (($row = fgetcsv($handle)) !== false) {
    $data[] = array_combine($header, $row);
}

// array_zip (no built-in, but easy to build)
function arrayZip(array ...$arrays): array
{
    return array_map(null, ...$arrays);
}

$names  = ['Budi', 'Siti', 'Dani'];
$scores = [85, 92, 78];
$zip    = arrayZip($names, $scores);
// [['Budi', 85], ['Siti', 92], ['Dani', 78]]

// range() with arrays
$evens  = range(2, 20, 2);  // [2,4,6,...,20]
$letters = range('a', 'z');  // ['a','b',...,'z']
$backwards = range(10, 1, -1); // [10,9,8,...,1]

// list() / destructuring in loops
$coordinates = [[1, 2], [3, 4], [5, 6]];
foreach ($coordinates as [$x, $y]) {
    echo "($x, $y)\n";
}

// array_map with null — matrix transpose
$matrix    = [[1, 2, 3], [4, 5, 6], [7, 8, 9]];
$transpose = array_map(null, ...$matrix);
// [[1,4,7], [2,5,8], [3,6,9]]

Summary #

  • array_multisort for multi-column sorting with type flags (SORT_STRING, SORT_NUMERIC, SORT_NATURAL) — alternatively use usort with array comparison via the spaceship operator for more readable code.
  • array_walk_recursive for transforming nested arrays — very useful for normalizing hierarchical configs, interpolating env variables, or multi-level data sanitization.
  • The array_u* functions (array_udiff, array_uintersect, array_udiff_assoc) accept custom comparators — use them for set operations on arrays of objects/associative arrays by specific fields.
  • array_fill_keys for initializing arrays with default values — cleaner than a loop for preparing counters, empty forms, or data templates.
  • array_splice vs unsetsplice reindexes the array (continuous indexes), unset leaves gaps. Choose splice when continuous numeric indexes matter.
  • compact for building arrays from variables — short and safe. Avoid extract on user data because it can inject variables into the scope unsafely.
  • Pipeline patterns make chains of array transformations easier to read — wrap them in a class with method chaining for expressive, testable code.
  • array_count_values for frequency analysis — count each value’s occurrences in one call, useful for tag clouds, top-N analysis, and simple statistics.

← Previous: Filter & Validation   Next: XML →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact