[php] PHP's array_map including keys

Is there a way of doing something like this:

$test_array = array("first_key" => "first_value", 
                    "second_key" => "second_value");

var_dump(array_map(function($a, $b) { return "$a loves $b"; }, 
         array_keys($test_array), 
         array_values($test_array)));

But instead of calling array_keys and array_values, directly passing the $test_array variable?

The desired output is:

array(2) {
  [0]=>
  string(27) "first_key loves first_value"
  [1]=>
  string(29) "second_key loves second_value"
}

This question is related to php functional-programming

The answer is


Another way of doing this with(out) preserving keys:

$test_array = [
    "first_key"     => "first_value",
    "second_key"    => "second_value"
];

$f = function($ar) {
    return array_map(
        function($key, $val) {
            return "{$key} - {$val}";
        },
        array_keys($ar),
        $ar
    );
};

#-- WITHOUT preserving keys
$res = $f($test_array);

#-- WITH preserving keys
$res = array_combine(
    array_keys($test_array),
    $f($test_array)
);

I see it's missing the obvious answer:

function array_map_assoc(){
    if(func_num_args() < 2) throw new \BadFuncionCallException('Missing parameters');

    $args = func_get_args();
    $callback = $args[0];

    if(!is_callable($callback)) throw new \InvalidArgumentException('First parameter musst be callable');

    $arrays = array_slice($args, 1);

    array_walk($arrays, function(&$a){
        $a = (array)$a;
        reset($a);
    });

    $results = array();
    $max_length = max(array_map('count', $arrays));

    $arrays = array_map(function($pole) use ($max_length){
        return array_pad($pole, $max_length, null);
    }, $arrays);

    for($i=0; $i < $max_length; $i++){
        $elements = array();
        foreach($arrays as &$v){
            $elements[] = each($v);
        }
        unset($v);

        $out = call_user_func_array($callback, $elements);

        if($out === null) continue;

        $val = isset($out[1]) ? $out[1] : null;

        if(isset($out[0])){
            $results[$out[0]] = $val;
        }else{
            $results[] = $val;
        }
    }

    return $results;
}

Works exactly like array_map. Almost.

Actually, it's not pure map as you know it from other languages. Php is very weird, so it requires some very weird user functions, for we don't want to unbreak our precisely broken worse is better approach.

Really, it's not actually map at all. Yet, it's still very useful.

  • First obvious difference from array_map, is that the callback takes outputs of each() from every input array instead of value alone. You can still iterate through more arrays at once.

  • Second difference is the way the key is handled after it's returned from callback; the return value from callback function should be array('new_key', 'new_value'). Keys can and will be changed, same keys can even cause previous value being overwritten, if same key was returned. This is not common map behavior, yet it allows you to rewrite keys.

  • Third weird thing is, if you omit key in return value (either by array(1 => 'value') or array(null, 'value')), new key is going to be assigned, as if $array[] = $value was used. That isn't map's common behavior either, yet it comes handy sometimes, I guess.

  • Fourth weird thing is, if callback function doesn't return a value, or returns null, the whole set of current keys and values is omitted from the output, it's simply skipped. This feature is totally unmappy, yet it would make this function excellent stunt double for array_filter_assoc, if there was such function.

  • If you omit second element (1 => ...) (the value part) in callback's return, null is used instead of real value.

  • Any other elements except those with keys 0 and 1 in callback's return are ignored.

  • And finally, if lambda returns any value except of null or array, it's treated as if both key and value were omitted, so:

    1. new key for element is assigned
    2. null is used as it's value
WARNING:
Bear in mind, that this last feature is just a residue of previous features and it is probably completely useless. Relying on this feature is highly discouraged, as this feature is going to be randomly deprecated and changed unexpectedly in future releases.

NOTE:
Unlike in array_map, all non-array parameters passed to array_map_assoc, with the exception of first callback parameter, are silently casted to arrays.

EXAMPLES:
// TODO: examples, anyone?


I'll add yet another solution to the problem using version 5.6 or later. Don't know if it's more efficient than the already great solutions (probably not), but to me it's just simpler to read:

$myArray = [
    "key0" => 0,
    "key1" => 1,
    "key2" => 2
];

array_combine(
    array_keys($myArray),
    array_map(
        function ($intVal) {
            return strval($intVal);
        },
        $myArray
    )
);

Using strval() as an example function in the array_map, this will generate:

array(3) {
  ["key0"]=>
  string(1) "0"
  ["key1"]=>
  string(1) "1"
  ["key2"]=>
  string(1) "2"
}

Hopefully I'm not the only one who finds this pretty simple to grasp. array_combine creates a key => value array from an array of keys and an array of values, the rest is pretty self explanatory.


I made this function, based on eis's answer:

function array_map_($callback, $arr) {
    if (!is_callable($callback))
        return $arr;

    $result = array_walk($arr, function(&$value, $key) use ($callback) {
        $value = call_user_func($callback, $key, $value);
    });

    if (!$result)
        return false;

    return $arr;
}

Example:

$test_array = array("first_key" => "first_value", 
                "second_key" => "second_value");

var_dump(array_map_(function($key, $value){
    return $key . " loves " . $value;
}, $arr));

Output:

array (
  'first_key' => 'first_key loves first_value,
  'second_key' => 'second_key loves second_value',
)

Off course, you can use array_values to return exactly what OP wants.

array_values(array_map_(function($key, $value){
    return $key . " loves " . $value;
}, $test_array))

With PHP5.3 or later:

$test_array = array("first_key" => "first_value", 
                    "second_key" => "second_value");

var_dump(
    array_map(
        function($key) use ($test_array) { return "$key loves ${test_array[$key]}"; },
        array_keys($test_array)
    )
);

A closure would work if you only need it once. I'd use a generator.

$test_array = [
    "first_key" => "first_value", 
    "second_key" => "second_value",
];

$x_result = (function(array $arr) {
    foreach ($arr as $key => $value) {
        yield "$key loves $value";
    }
})($test_array);

var_dump(iterator_to_array($x_result));

// array(2) {
//   [0]=>
//   string(27) "first_key loves first_value"
//   [1]=>
//   string(29) "second_key loves second_value"
// }

For something reusable:

function xmap(callable $cb, array $arr)
{
    foreach ($arr as $key => $value) {
        yield $cb($key, $value);
    }
}

var_dump(iterator_to_array(
    xmap(function($a, $b) { return "$a loves $b"; }, $test_array)
));

By "manual loop" I meant write a custom function that uses foreach. This returns a new array like array_map does because the function's scope causes $array to be a copy—not a reference:

function map($array, callable $fn) {
  foreach ($array as $k => &$v) $v = call_user_func($fn, $k, $v);
  return $array;
}

Your technique using array_map with array_keys though actually seems simpler and is more powerful because you can use null as a callback to return the key-value pairs:

function map($array, callable $fn = null) {
  return array_map($fn, array_keys($array), $array);
}

Based on eis's answer, here's what I eventually did in order to avoid messing the original array:

$test_array = array("first_key" => "first_value",
                    "second_key" => "second_value");

$result_array = array();
array_walk($test_array, 
           function($a, $b) use (&$result_array) 
           { $result_array[] = "$b loves $a"; }, 
           $result_array);
var_dump($result_array);

This is how I've implemented this in my project.

function array_map_associative(callable $callback, $array) {
    /* map original array keys, and call $callable with $key and value of $key from original array. */
    return array_map(function($key) use ($callback, $array){
        return $callback($key, $array[$key]);
    }, array_keys($array));
}

Look here! There is a trivial solution!

function array_map2(callable $f, array $a)
{
    return array_map($f, array_keys($a), $a);
}

As stated in the question, array_map already has exactly the functionality required. The other answers here seriously overcomplicate things: array_walk is not functional.

Usage

Exactly as you would expect from your example:

$test_array = array("first_key" => "first_value", 
                    "second_key" => "second_value");

var_dump(array_map2(function($a, $b) { return "$a loves $b"; }, $test_array));

This is probably the shortest and easiest to reason about:

$states = array('az' => 'Arizona', 'al' => 'Alabama');

array_map(function ($short, $long) {
    return array(
        'short' => $short,
        'long'  => $long
    );
}, array_keys($states), $states);

// produces:
array(
     array('short' => 'az', 'long' => 'Arizona'), 
     array('short' => 'al', 'long' => 'Alabama')
)

YaLinqo library* is well suited for this sort of task. It's a port of LINQ from .NET which fully supports values and keys in all callbacks and resembles SQL. For example:

$mapped_array = from($test_array)
    ->select(function ($v, $k) { return "$k loves $v"; })
    ->toArray();

or just:

$mapped_iterator = from($test_array)->select('"$k loves $v"');

Here, '"$k loves $v"' is a shortcut for full closure syntax which this library supports. toArray() in the end is optional. The method chain returns an iterator, so if the result just needs to be iterated over using foreach, toArray call can be removed.

* developed by me


$array = [
  'category1' => 'first category',
  'category2' => 'second category',
];
 
$new = array_map(function($key, $value) {
  return "{$key} => {$value}";
}, array_keys($array), $array);

You can use map method from this array library to achieve exactly what you want as easily as:

Arr::map($test_array, function($a, $b) { return "$a loves $b"; });

also it preserves keys and returns new array, not to mention few different modes to fit your needs.


I'd do something like this:

<?php

/**
 * array_map_kv()
 *   An array mapping function to map with both keys and values.
 *
 * @param $callback callable
 *   A callback function($key, $value) for mapping values.
 * @param $array array
 *   An array for mapping.
 */
function array_map_kv(callable $callback, array $array) {
  return array_map(
    function ($key) use ($callback, $array) {
      return $callback($key, $array[$key]); // $callback($key, $value)
    },
    array_keys($array)
  );
}

// use it
var_dump(array_map_kv(function ($key, $value) {
  return "{$key} loves {$value}";
}, array(
  "first_key" => "first_value",
  "second_key" => "second_value",
)));

?>

Results:

array(2) {
  [0]=>
  string(27) "first_key loves first_value"
  [1]=>
  string(29) "second_key loves second_value"
}

Here's my very simple, PHP 5.5-compatible solution:

function array_map_assoc(callable $f, array $a) {
    return array_column(array_map($f, array_keys($a), $a), 1, 0);
}

The callable you supply should itself return an array with two values, i.e. return [key, value]. The inner call to array_map therefore produces an array of arrays. This then gets converted back to a single-dimension array by array_column.

Usage

$ordinals = [
    'first' => '1st',
    'second' => '2nd',
    'third' => '3rd',
];

$func = function ($k, $v) {
    return ['new ' . $k, 'new ' . $v];
};

var_dump(array_map_assoc($func, $ordinals));

Output

array(3) {
  ["new first"]=>
  string(7) "new 1st"
  ["new second"]=>
  string(7) "new 2nd"
  ["new third"]=>
  string(7) "new 3rd"
}

Partial application

In case you need to use the function many times with different arrays but the same mapping function, you can do something called partial function application (related to ‘currying’), which allows you to only pass in the data array upon invocation:

function array_map_assoc_partial(callable $f) {
    return function (array $a) use ($f) {
        return array_column(array_map($f, array_keys($a), $a), 1, 0);
    };
}

...
$my_mapping = array_map_assoc_partial($func);
var_dump($my_mapping($ordinals));

Which produces the same output, given $func and $ordinals are as earlier.

NOTE: if your mapped function returns the same key for two different inputs, the value associated with the later key will win. Reverse the input array and output result of array_map_assoc to allow earlier keys to win. (The returned keys in my example cannot collide as they incorporate the key of the source array, which in turn must be unique.)


Alternative

Following is a variant of the above, which might prove more logical to some, but requires PHP 5.6:

function array_map_assoc(callable $f, array $a) {
    return array_merge(...array_map($f, array_keys($a), $a));
}

In this variant, your supplied function (over which the data array is mapped) should instead return an associative array with one row, i.e. return [key => value]. The result of mapping the callable is then simply unpacked and passed to array_merge. As earlier, returning a duplicate key will result in later values winning.

n.b. Alex83690 has noted in a comment that using array_replace here in the stead of array_merge would preserve integer keys. array_replace does not modify the input array, so is safe for functional code.

If you are on PHP 5.3 to 5.5, the following is equivalent. It uses array_reduce and the binary + array operator to convert the resulting two-dimensional array down to a one-dimensional array whilst preserving keys:

function array_map_assoc(callable $f, array $a) {
    return array_reduce(array_map($f, array_keys($a), $a), function (array $acc, array $a) {
        return $acc + $a;
    }, []);
}

Usage

Both of these variants would be used thus:

$ordinals = [
    'first' => '1st',
    'second' => '2nd',
    'third' => '3rd',
];

$func = function ($k, $v) {
    return ['new ' . $k => 'new ' . $v];
};

var_dump(array_map_assoc($func, $ordinals));

Note the => instead of , in $func.

The output is the same as before, and each can be partially applied in the same way as before.


 Summary

The goal of the original question is to make the invocation of the call as simple as possible, at the expense of having a more complicated function that gets invoked; especially, to have the ability to pass the data array in as a single argument, without splitting the keys and values. Using the function supplied at the start of this answer:

$test_array = ["first_key" => "first_value",
               "second_key" => "second_value"];

$array_map_assoc = function (callable $f, array $a) {
    return array_column(array_map($f, array_keys($a), $a), 1, 0);
};

$f = function ($key, $value) {
    return [$key, $key . ' loves ' . $value];
};

var_dump(array_values($array_map_assoc($f, $test_array)));

Or, for this question only, we can make a simplification to array_map_assoc() function that drops output keys, since the question does not ask for them:

$test_array = ["first_key" => "first_value",
               "second_key" => "second_value"];

$array_map_assoc = function (callable $f, array $a) {
    return array_map($f, array_keys($a), $a);
};

$f = function ($key, $value) {
    return $key . ' loves ' . $value;
};

var_dump($array_map_assoc($f, $test_array));

So the answer is NO, you can't avoid calling array_keys, but you can abstract out the place where array_keys gets called into a higher-order function, which might be good enough.


I always like the javascript variant of array map. The most simple version of it would be:

/**
 * @param  array    $array
 * @param  callable $callback
 * @return array
 */
function arrayMap(array $array, callable $callback)
{
    $newArray = [];

    foreach( $array as $key => $value )
    {
        $newArray[] = call_user_func($callback, $value, $key, $array);
    }

    return $newArray;
}

So now you can just pass it a callback function how to construct the values.

$testArray = [
    "first_key" => "first_value", 
    "second_key" => "second_value"
];

var_dump(
    arrayMap($testArray, function($value, $key) {
        return $key . ' loves ' . $value;
    });
);