27

I'm sure this question has been asked before, my apologies for not finding it first.

The original array:

[0] => Array
    (
        [categoryId] => 1
        [eventId] => 2
        [eventName] => 3
        [vendorName] => 4
    )

[1] => Array
    (
        [categoryId] => 5
        [eventId] => 6
        [eventName] => 7
        [vendorName] => 8
    )

[2] => Array
    (
        [categoryId] => 9
        [eventId] => 10
        [eventName] => 11
        [vendorName] => 12
    )

My hoped for result out of: print_r(get_values_from_a_key_in_arrays('categoryId', $array));

[0] => 1
[1] => 5
[2] => 9

I'm just looking for something cleaner than writing my own foreach based function. If foreach is the answer, I already have that in place.

Edit: I don't want to use a hard-coded key, I was just showing an example call to the solution. Thanks! ^_^

Quick Grab Solution for PHP 5.3:

private function pluck($key, $data) {
    return array_reduce($data, function($result, $array) use($key) {
        isset($array[$key]) && $result[] = $array[$key];
        return $result;
    }, array());
}
Wil Moore III
  • 6,968
  • 3
  • 36
  • 49
Caleb Gray
  • 3,040
  • 2
  • 21
  • 32
  • 2
    This is known as `plucking`, and is not a default PHP function. You can easily write it yourself though. – Joost May 18 '12 at 21:19
  • Really cool. I've done this before, but I never knew that the technical name for it was `plucking`. – Moses May 18 '12 at 21:45
  • This is nice, but `array_reduce()` is the WRONG function. It's intended to "reduce the array to a single value." This is also true in other languages. You should rewrite with `array_map()` or `array_walk()` ...which will also simplify the code. – doublejosh Jun 24 '16 at 21:56
  • @doublejosh I disagree. `array_reduce` is fine here. It's reducing an array of arrays to an array of values. – Michael Jul 27 '16 at 23:13
  • The inner operation is indeed a reduce, I must have looked at the outer operation when I complained about the usage. – doublejosh Jul 27 '16 at 23:16

8 Answers8

34

So, the cool thing about higher-order collection/iterator functions such as pluck, filter, each, map, and friends is that they can be mixed and matched to compose a more complex set of operations.

Most languages provide these types of functions (look for packages like collection, iterator, or enumeration/enumerable)...some provide more functions than others and you will commonly see that the functions are named differently across languages (i.e. collect == map, reduce == fold). If a function doesn't exist in your language, you can create it from the ones that do exist.

As for your test case...we can use array_reduce to implement pluck. The first version I posted relied on array_map; however, I agree with @salathe that array_reduce is more succinct for this task; array_map is an OK option, but you end up having to do more work in the end. array_reduce can look a bit odd at first, but if the callback is neatly organized, all is well.

A less naive pluck would also check to see if it can "call" (a function/method) on the iterated value. In the naive implementation below, we assume the structure to be a hash (associative array).

This will setup the test-case data (Fixtures):

<?php

$data[] = array('categoryId' => 1,    'eventId' => 2,  'eventName' => 3,  'vendorName' => 4);
$data[] = array('categoryId' => 5,    'eventId' => 6,  'eventName' => 7,  'vendorName' => 8);
$data[] = array('categoryId' => 9,    'eventId' => 10, 'eventName' => 11, 'vendorName' => 12);
$data[] = array(/* no categoryId */   'eventId' => 10, 'eventName' => 11, 'vendorName' => 12);
$data[] = array('categoryId' => false,'eventId' => 10, 'eventName' => 11, 'vendorName' => 12);
$data[] = array('categoryId' => 0.0,  'eventId' => 10, 'eventName' => 11, 'vendorName' => 12);

Choose the version of pluck you'd prefer

$preferredPluck = 'pluck_array_reduce'; // or pluck_array_map

"pluck" for PHP 5.3+: array_reduce provides a terse implementation though not as easy to reason about as the array_map version:

function pluck_array_reduce($key, $data) {
  return array_reduce($data, function($result, $array) use($key){
    isset($array[$key]) &&
      $result[] = $array[$key];

    return $result;
  }, array());
}

"pluck" for PHP 5.3+: array_map isn't perfect for this so we have to do more checking (and it still doesn't account for many potential cases):

function pluck_array_map($key, $data) {
  $map = array_map(function($array) use($key){
    return isset($array[$key]) ? $array[$key] : null;
  }, $data);

  // is_scalar isn't perfect; to make this right for you, you may have to adjust
  return array_filter($map, 'is_scalar');
}

"pluck" for legacy PHP <5.3

We could have used the legacy create_function; however, it is bad form, not recommended, and also not at all elegant, thus, I've decided not to show it.

function pluck_compat($key, $data) {
  $map = array();
  foreach ($data as $array) {
    if (array_key_exists($key, $array)) {
      $map[] = $array[$key];
    }
  }
  unset($array);

  return $map;
}

Here we choose a version of "pluck" to call based on the version of PHP we are running. If you run the entire script, you should get the correct answer no matter what version you are on.

$actual   = version_compare(PHP_VERSION, '5.3.0', '>=')
          ? $preferredPluck('categoryId', $data)
          : pluck_compat('categoryId', $data);
$expected = array(1, 5, 9, false, 0.0);
$variance = count(array_diff($expected, $actual));

var_dump($expected, $actual);
echo PHP_EOL;
echo 'variance: ', $variance, PHP_EOL;

print @assert($variance)
    ? 'Assertion Failed'
    : 'Assertion Passed';

Notice there is no ending '?>'. That is because it isn't needed. More good can come of leaving it off than from keeping it around.

FWIW, it looks like this is being added to PHP 5.5 as array_column.

Community
  • 1
  • 1
Wil Moore III
  • 6,968
  • 3
  • 36
  • 49
  • 1
    Woah, what version of PHP is this? I've never seen 'use', that's pretty awesome. – Caleb Gray May 18 '12 at 21:39
  • use is provided in PHP 5.3 when they introduced anonymous functions (lambdas and closures): http://php.net/functions.anonymous – Wil Moore III May 18 '12 at 21:49
  • For the sake of others that find this, do you mind using create_function, and just putting the array_map inside the array_filter call (getting rid of the $map variable)? – Caleb Gray May 18 '12 at 21:50
  • At this point, I'm wondering if your version or tadeck's benchmarks better. If you incorporate an isset sanity check, use create_function for PHP 4 compatibility, and show that your version runs faster than I'll choose your answer. – Caleb Gray May 18 '12 at 22:11
  • 1
    P.S. The map/filter step can be done in one with [`array_reduce()`](http://php.net/array_reduce). – salathe May 18 '12 at 22:17
  • Hi @CalebGray, I've prototyped a few versions and I ended up not being comfortable with the create_function version. I've noted my reasons in the updated answer. – Wil Moore III May 18 '12 at 22:29
  • 1
    Hi @salathe; you brought up a good point. I almost did my original implementation with array_reduce(); however, I generally reserve array_reduce for situations that can't be handled easily with something else. array_reduce is awesome; however, it takes just a tad more cognitive overhead to reason about. – Wil Moore III May 18 '12 at 22:30
  • @wilmoore it wouldn't take any more cognitive overhead than your (frankly strange) choice of a bare `array_filter()` after the map. – salathe May 18 '12 at 22:32
  • @CalebGray, if it is all about the benchmark, then I happily accept that perhaps another entry may satisfy your requirement better. I generally would not try to optimize such a thing in the real-world. If this function needed optimization, I'd likely pull the needed functionality into a C extension. I'd get more bang for my buck that way. But in that case, I may as well write the app with JRuby so I'd get the speed of the JVM in the first place and a better language (Ruby) to boot :) – Wil Moore III May 18 '12 at 22:33
  • Hah, fair enough salathe. Thank you wilmoore, I've accepted your excellent answer. – Caleb Gray May 18 '12 at 22:33
  • @CalebGray, thanks for accepting and thanks for the thoughtful question. – Wil Moore III May 18 '12 at 22:34
  • @wilmoore your two functions give differing results with "falsey" (not-)plucked values. – salathe May 18 '12 at 22:38
  • Hi @salathe, I suppose the bare array_filter() is a bit odd; however, it is just the way PHP is. I wish it were not that way. It is to drop the null values. This is the default behavior :) – Wil Moore III May 18 '12 at 22:39
  • @wilmoore use `"is_null"` with the filter (but then my previous comment would still stand, but with "null" values). – salathe May 18 '12 at 22:40
  • @salathe: If he uses `is_null` as the callback then only `NULL` values **won't** be filtered. I think `strlen` would be a more appropriate callback, albeit a bit *unsemantic*. – Alix Axel May 18 '12 at 23:11
  • Oh, like that... Then you might as well use `isset()` instead. – Alix Axel May 18 '12 at 23:12
  • @AlixAxel Yes, you are correct. Using `is_null` as the callback leaves you with only NULL values in the returned list. I also agree that strlen (while it would probably work) would be less than intention-revealing. I ended up going more verbose with a callback that checks for !is_null. – Wil Moore III May 18 '12 at 23:13
  • @wilmoore: Yeah, I didn't realized that the callback was supposed to be a lambda function too. Either way `!is_null() === isset()`, other than that good job. ;) – Alix Axel May 18 '12 at 23:19
  • `is_scalar()` doesn't help with non-scalar values (well duh!) that the array could happily contain. (P.S. there was some disconnect between my fingers and brain; I didn't intend to say to use `is_null` as the filter but in the filter.) – salathe May 19 '12 at 10:52
  • If your on a hosting solution that allows you to upgrade to PHP 5.5 through a config file, the `array_column` function is just what your looking for. Just be sure to read the [php documentation for upgrading](http://php.net/manual/en/migration55.php) – Nick Gronow Nov 05 '14 at 15:32
  • @NicholasGronow Did you downvote the answer because you hate answers that weren't written just last week? :)- – Wil Moore III Nov 05 '14 at 23:06
  • I actually upvoted this answer :) And since I try to encourage people to stay up-to-date with with PHP on their hosting solutions, I figured I would spell it out a bit more in a comment. Nice and comprehensive answer wilmoore. – Nick Gronow Nov 07 '14 at 11:55
  • Cool, thanks @NicholasGronow. I guess it was someone else. It's really hard to tell; though, I am probably just missing something since I don't spend a ton of time on SO. Either way, thanks :) – Wil Moore III Nov 07 '14 at 22:37
  • can you explain why you unset the `$array` variable before returning in the 5.3+ example? I feel like the GC should take care of this for you.. – jrz Mar 06 '15 at 05:38
  • @Jonz It's been a while since I've written any PHP; however, I do recall that `foreach` will leak the variable outside of the block. Sure, once the function is out of scope, the GC should clean up. Explicitly unsetting is not necessary; however, I also don't like to drop litter even though I know it will likely end up being picked up; so, there you go :) – Wil Moore III Mar 06 '15 at 16:56
9

Mapping is what you need:

$input = array(
    array(
        'categoryId' => 1,
        'eventId' => 2,
        'eventName' => 3,
        'vendorName' => 4,
    ),
    array(
        'categoryId' => 5,
        'eventId' => 6,
        'eventName' => 7,
        'vendorName' => 8,
    ),
    array(
        'categoryId' => 9,
        'eventId' => 10,
        'eventName' => 11,
        'vendorName' => 12,
    ),
);

$result = array_map(function($val){
    return $val['categoryId'];
}, $input);

Or creating a function you wanted:

function get_values_from_a_key_in_arrays($key, $input){
    return array_map(function($val) use ($key) {
        return $val[$key];
    }, $input);
};

and then using it:

$result = get_values_from_a_key_in_arrays('categoryId', $array);

It will work in PHP >= 5.3, where anonymous callbacks are allowed. For earlier versions you will need to define callback earlier and pass its name instead of anonymous function.

salathe
  • 51,324
  • 12
  • 104
  • 132
Tadeck
  • 132,510
  • 28
  • 152
  • 198
  • can you make `return $val['categoryId'];` to be `return $val[$key];` ? – Songo May 18 '12 at 21:47
  • 1
    @Songo: Not without creating an array consisting of `count($input)` values of `$key`, one solution would be `array_map(function($val, $key = 'categoryId') {return $val[$key];}, $input)` -- but I can't see any benefit in doing that in a lambda function. – Alix Axel May 18 '12 at 21:51
  • @Songo: The other solution would be: `array_map(function($val, $key) {return $val[$key];}, $input, array_fill(0, count($input), 'categoryId'))`. – Alix Axel May 18 '12 at 21:53
  • 1
    @Songo: I have added a function exactly as OP requested, so you should be satisfied ;) – Tadeck May 18 '12 at 21:59
  • @AlixAxel: Thanks, but first is rather redundant, as `$key` is never set, so basically this is a way of writing what I did in different manner. Second solution in turn is an overkill leading to decreased code readability. Sorry, I took my own path enclosing it in a different function - hope you will like it. – Tadeck May 18 '12 at 22:03
  • 1
    Don't use `create_function()`, if don't want `array_map()` with an anonymous function: instead, use a loop (which will be "faster" than calling the map callback anyway). – salathe May 18 '12 at 22:15
  • @AlixAxel in your first solution how to pass `$key` value to the lambda function? – Songo May 18 '12 at 22:17
  • @Songo see his "other solution". – salathe May 18 '12 at 22:20
  • @salathe Aha!! now I get it :D no wonder why I couldn't get it to work :))) – Songo May 18 '12 at 22:55
  • @Tadeck: Yeah, that was my point precisely. I was just addressing Songos' doubt. – Alix Axel May 18 '12 at 23:15
  • @AlixAxel: Ok, I got used to natural way of doing callbacks like in Python or JavaScript and forgot about PHP's closures implementation and `use` ;) Thanks, salathe. – Tadeck May 18 '12 at 23:26
6

There's no built-in function for this, but it's usually referred as "pluck".

Alix Axel
  • 151,645
  • 95
  • 393
  • 500
5
<?php
$a = array(
        array('a' => 1, 'b' => 2),
        array('a' => 2, 'b' => 2),
        array('a' => 3, 'b' => 2),
        array('a' => 4, 'b' => 2)
);

function get_a($v) {
        return $v['a'];
}

var_dump(array_map('get_a', $a));

You can use an create_function or an anonymous function (PHP 5.3 >=)

<?php
$a = array(
        array('a' => 1, 'b' => 2),
        array('a' => 2, 'b' => 2),
        array('a' => 3, 'b' => 2),
        array('a' => 4, 'b' => 2)
);

var_dump(array_map(create_function('$v', 'return $v["a"];'), $a));

I'd write a callback function, as above, and then use it with array_map.

Rawkode
  • 21,990
  • 5
  • 38
  • 45
4

As of PHP 5.5, use array_column:

$events = [
    [ 'categoryId' => 1, 'eventId' =>  2, 'eventName' =>  3, 'vendorName' =>  4 ],
    [ 'categoryId' => 5, 'eventId' =>  6, 'eventName' =>  7, 'vendorName' =>  8 ],
    [ 'categoryId' => 9, 'eventId' => 10, 'eventName' => 11, 'vendorName' => 12 ],
];

print_r(array_column($events, 'categoryId'));

See it online at 3v4l.

For versions before 5.5, you may consider using a polyfill.

bishop
  • 37,830
  • 11
  • 104
  • 139
3

There's no built in function. But one is easily made with array_map().

$array = array(
    array(
        "categoryID"   => 1,
        "CategoryName" => 2,
        "EventName"    => 3,
        "VendorName"   => 4
    ),
    array(
        "categoryID"   => 5,
        "CategoryName" => 6,
        "EventName"    => 7,
        "VendorName"   => 8
    ),
    array(
        "categoryID"   => 9,
        "CategoryName" => 10,
        "EventName"    => 11,
        "VendorName"   => 12
    )
);

$newArray = array_map(function($el) {
    return $el["categoryID"];
}, $array);

var_dump($newArray);
Madara's Ghost
  • 172,118
  • 50
  • 264
  • 308
2

Where is lisp when you need it? Actually in php it is pretty easy to manage too. Just use the array_map function as illustrated below.

# bunch o data
$data = array();
$data[0] = array("id" => 100, "name" => 'ted');
$data[1] = array("id" => 200, "name" => 'mac');
$data[2] = array("id" => 204, "name" => 'bub');

# what you want to do to each bit of it
function pick($n) { return($n['id']); }
# what you get after you do that
$map = array_map("pick", $data);
# see for yourself
print_r($map);
zortacon
  • 617
  • 5
  • 15
0

You could use array_filter, and pass in a function based on the desired key.

Tadeck's answer is way better though.

Turnsole
  • 3,422
  • 5
  • 30
  • 52