Borek Bernard
Borek Bernard

Reputation: 53241

array_map with access to keys

I need to iterate over an array that looks like this:

$myArray = array(
  'key1' => array('subkey' => 'subvalue'),
  'key2' => array('subkey' => 'subvalue'),
)

Into each nested associative array, I want to add a key-value pair based on the outside key, like this:

$myNewArray = array(
  'key1' => array('subkey' => 'subvalue', 'newkey' => 'only for key1'),
  'key2' => array('subkey' => 'subvalue'),
)

Ideally, I'm looking for something like:

$myNewArray = array_map(function($key, $value) {
    if ($key == 'key1') {
        $value['newkey'] = 'only for key1';
    }
    return $value;
}, $myArray);

However, that obviously doesn't work as callback isn't given two parameters but only one. I could do something like this:

$myNewArray = array_map(function($key, $value) {
    if ($key == 'key1') {
        $value['newkey'] = 'only for key1';
    }
    return WHAT??
}, array_keys($myArray), $myArray);

However, what do I return here? It seems to always construct a new array, i.e. discarding my string keys (key1 and key2), while a single-array array_map() keeps them.

I can use array_walk() but it has a rather strange API (flipped parameter order, array passed by reference etc.) so I would generally prefer if this could be achieved using array_map(). Can it?

Upvotes: 6

Views: 18100

Answers (4)

Brad
Brad

Reputation: 950

Use a custom fucntion.

I know I am late to the party but, the answer is so simple if you can define your own function.

function array_map_kv(array $array, callable $callback): array
{
    $newArray = [];
    foreach ($array as $key => $value) {
        $newArray += $callback($key, $value);
    }
    return $newArray;
}

Super easy to understand and use, just return an array in your callback:

//DEMO of associative array map function
$oldArray = [
    'Chicky!' => null,
    'not part of the song' => 'blah, blah, blah',
    'My name is Cha-Cha!' => 'Clap, clap, cha-cha-cha'
];

$newArray = array_map_kv($oldArray, function ($key, $value) {
    //modify key or value or both at once.
    if (empty($value)) return ['My name is Chicky!' => 'Chicky, Chicky, Chicky'];

    // you can exclude entries giving you the power of array_filter at the same time 😉.
    if ($key === 'not part of the song') return [];

    // or change nothing.
    return [$key => $value];
});

//the expected result.
assert($newArray == [
        'My name is Chicky!' => 'Chicky, Chicky, Chicky',
        'My name is Cha-Cha!' => 'Clap, clap, cha-cha-cha',
    ]);

Upvotes: 0

Serious Angel
Serious Angel

Reputation: 1555

Array transformation function variant, supporting keys:

/**
 * Transform array elements.
 *
 * @param array                      $a Original array.
 * @param callable(V, K, $a): V|null $vTransformer Value transformation function.
 * @param callable(K, V, $a): K|null $kTransformer Key transformation function.
 *
 * @template K Array key
 * @template V Array value
 *
 * @return array Transformed array.
 */
function transformArray(array $a, ?callable $vTransformer = null, ?callable $kTransformer = null): array
{
    $arrayKeys   = array_keys($a);
    $arrayValues = array_values($a);

    return array_combine(
        is_callable($kTransformer)
            ? array_map(fn ($k) => $kTransformer($k, $a[$k], $a), $arrayKeys, $arrayValues)
            : $arrayKeys,

        is_callable($vTransformer)
            ? array_map(fn ($k) => $vTransformer($a[$k], $k, $a), $arrayKeys, $arrayValues)
            : $arrayValues
    );
}

Example

$a = [
    'k1' => 100,
    'k2' => 101,
];

$b = transformArray($a,
    fn ($v) => $v + 10
);

$c = transformArray($a,
    null,
    fn ($k) => "k-$k"
);

$d = transformArray($a,
    fn ($v) => $v + 10,
    fn ($k) => "k-$k"
);

var_dump([
    'a' => $a,
    'b' => $b,
    'c' => $c,
    'd' => $d,
]);
array(2) {
  ["a"] => array(2) {
    ["k1"] => int(100)
    ["k2"] => int(101)
  }
  ["b"]=> array(2) {
    ["k1"] => int(110)
    ["k2"] => int(111)
  }
  ["c"]=> array(2) {
    ["k-k1"] => int(100)
    ["k-k2"] => int(101)
  }
  ["d"]=> array(2) {
    ["k-k1"] => int(110)
    ["k-k2"] => int(111)
  }
}

Upvotes: 0

AbraCadaver
AbraCadaver

Reputation: 78994

array_map only passes the value not the key. array_walk is very similar to the array_map. Just define the value as a reference. It takes the key where array_map does not. ALso, this modifies $myArray:

array_walk($myArray, function(&$value, $key) {
    if ($key == 'key1') {
        $value['newkey'] = 'only for key1';
    }
});

From the PHP manual, there does seem to be an array_map method using array_keys as another array:

$myNewArray = array_combine(array_keys($myArray), array_map(function($key, $value) {
    if ($key == 'key1') {
        $value['newkey'] = 'only for key1';
    }
    return  $value;
}, array_keys($myArray), $myArray));

Upvotes: 1

axiac
axiac

Reputation: 72226

I'm afraid array_walk() IS the way to do this.

If you don't like array_walk() and insist on doing it with array_map(), well, it's possible. It involves using also array_keys(), array_values() and array_combine(), it is long and ugly but doable:

$myNewArray = array_combine(
    array_keys($myArray),
    array_map(
        function($key, $value) {
            if ($key == 'key1') {
                $value['newkey'] = 'only for key1';
            }
            return $value;
        },
        array_keys($myArray),
        array_values($myArray)     // can omit array_values() and use $myArray
    )
);

You can also do it using array_reduce() but it's the same mess:

$myNewArray = array_reduce(
    array_keys($myArray),
    function (array $carry, $key) use ($myArray) {
        $value = $myArray[$key];
        if ($key == 'key1') {
            $value['newkey'] = 'only for key1';
        }
        $carry[$key] = $value;
        return $carry;
    },
    array()
);

I hope you have a condition more complex than $key == 'key1' because only for this it is not worth it writing complex traversal. Isn't it easier to just access the right element and modify it?

$myArray['key1']['newkey'] = 'only for key1';

Upvotes: 4

Related Questions