How to filter an associative array comparing keys with values in an indexed array?
Asked Answered
M

12

457

The callback function in array_filter() only passes in the array's values, not the keys.

If I have:

$my_array = array("foo" => 1, "hello" => "world");

$allowed = array("foo", "bar");

What's the best way to delete all keys in $my_array that are not in the $allowed array?

Desired output:

$my_array = array("foo" => 1);
Mclellan answered 23/11, 2010 at 19:41 Comment(1)
Not a solution but another approach that might be useful is to $b = ['foo' => $a['foo'], 'bar' => $a['bar']] This will result in $b['bar'] be null.Sooty
A
472

PHP 5.6 introduced a third parameter to array_filter(), flag, that you can set to ARRAY_FILTER_USE_KEY to filter by key instead of value:

$my_array = ['foo' => 1, 'hello' => 'world'];
$allowed  = ['foo', 'bar'];
$filtered = array_filter(
    $my_array,
    function ($key) use ($allowed) {
        // N.b. in_array() is notorious for being slow 
        return in_array($key, $allowed);
    },
    ARRAY_FILTER_USE_KEY
);

Since PHP 7.4 introduced arrow functions we can make this more succinct:

$my_array = ['foo' => 1, 'hello' => 'world'];
$allowed  = ['foo', 'bar'];
$filtered = array_filter(
    $my_array,
    fn ($key) => in_array($key, $allowed),
    ARRAY_FILTER_USE_KEY
);

Clearly this isn't as elegant as array_intersect_key($my_array, array_flip($allowed)), but it does offer the additional flexibility of performing an arbitrary test against the key, e.g. $allowed could contain regex patterns instead of plain strings.

You can also use ARRAY_FILTER_USE_BOTH to have both the value and the key passed to your filter function. Here's a contrived example based upon the first, but note that I'd not recommend encoding filtering rules using $allowed this way:

$my_array = ['foo' => 1, 'bar' => 'baz', 'hello' => 'wld'];
$allowed  = ['foo' => true, 'bar' => true, 'hello' => 'world'];
$filtered = array_filter(
    $my_array,
    fn ($val, $key) => isset($allowed[$key]) && (
        $allowed[$key] === true || $allowed[$key] === $val
    ),
    ARRAY_FILTER_USE_BOTH
); // ['foo' => 1, 'bar' => 'baz']
Aryanize answered 23/10, 2014 at 8:37 Comment(5)
Damn, as the author of that feature I should have looked for this question ;-)Hands
PHP 7.4+ $filtered = array_filter( $my_array, fn ($key) => in_array($key, $allowed), ARRAY_FILTER_USE_KEY );Jean
Any answer that leverages iterated calls of in_array() will not be more efficient than the more elegant call of array_intersect_key(). Yes, the lookup array will need to be flipped once, but because PHP is very fast about making key lookups (such as isset()), I expect in_array() to be left in the dust in the majority of test cases. More simply, isset() has been proven time and time again to greatly outperform in_array() in benchmarks. The only danger to be aware of is when the flipping technique mutates the value -- such as when you flip a float value into a key, it becomes an int.Pitiable
@Pitiable You’re likely to need to have a large array for the difference to be significant to the running of your application. Usually readability trumps performance micro-optimisations. Certainly something to be conscious of though.Aryanize
There are no redeeming features to your snippets and I would not use any of them in my own projects. VincentSavard's flip&intersect_key technique is more performant, more concise, more elegant, more readable, and suitably utilizes a fully native functional approach. I am not attacking you, I am comparing the posts.Pitiable
A
532

With array_intersect_key and array_flip:

var_dump(array_intersect_key($my_array, array_flip($allowed)));

array(1) {
  ["foo"]=>
  int(1)
}
Avisavitaminosis answered 23/11, 2010 at 19:46 Comment(3)
@GWW, Generally, I've found that these types of array functions are faster than the equivalent foreach loop (and sometimes considerably), but the only way to know for sure is to time them both on the same data.Symposiac
Why use array_flip? Simply define the $allowed with keys: allowed = array ( 'foo' => 1, 'bar' => 1 );Baugher
@YuvalA. If the array is larger, then array_flip gets necessaryLucre
A
472

PHP 5.6 introduced a third parameter to array_filter(), flag, that you can set to ARRAY_FILTER_USE_KEY to filter by key instead of value:

$my_array = ['foo' => 1, 'hello' => 'world'];
$allowed  = ['foo', 'bar'];
$filtered = array_filter(
    $my_array,
    function ($key) use ($allowed) {
        // N.b. in_array() is notorious for being slow 
        return in_array($key, $allowed);
    },
    ARRAY_FILTER_USE_KEY
);

Since PHP 7.4 introduced arrow functions we can make this more succinct:

$my_array = ['foo' => 1, 'hello' => 'world'];
$allowed  = ['foo', 'bar'];
$filtered = array_filter(
    $my_array,
    fn ($key) => in_array($key, $allowed),
    ARRAY_FILTER_USE_KEY
);

Clearly this isn't as elegant as array_intersect_key($my_array, array_flip($allowed)), but it does offer the additional flexibility of performing an arbitrary test against the key, e.g. $allowed could contain regex patterns instead of plain strings.

You can also use ARRAY_FILTER_USE_BOTH to have both the value and the key passed to your filter function. Here's a contrived example based upon the first, but note that I'd not recommend encoding filtering rules using $allowed this way:

$my_array = ['foo' => 1, 'bar' => 'baz', 'hello' => 'wld'];
$allowed  = ['foo' => true, 'bar' => true, 'hello' => 'world'];
$filtered = array_filter(
    $my_array,
    fn ($val, $key) => isset($allowed[$key]) && (
        $allowed[$key] === true || $allowed[$key] === $val
    ),
    ARRAY_FILTER_USE_BOTH
); // ['foo' => 1, 'bar' => 'baz']
Aryanize answered 23/10, 2014 at 8:37 Comment(5)
Damn, as the author of that feature I should have looked for this question ;-)Hands
PHP 7.4+ $filtered = array_filter( $my_array, fn ($key) => in_array($key, $allowed), ARRAY_FILTER_USE_KEY );Jean
Any answer that leverages iterated calls of in_array() will not be more efficient than the more elegant call of array_intersect_key(). Yes, the lookup array will need to be flipped once, but because PHP is very fast about making key lookups (such as isset()), I expect in_array() to be left in the dust in the majority of test cases. More simply, isset() has been proven time and time again to greatly outperform in_array() in benchmarks. The only danger to be aware of is when the flipping technique mutates the value -- such as when you flip a float value into a key, it becomes an int.Pitiable
@Pitiable You’re likely to need to have a large array for the difference to be significant to the running of your application. Usually readability trumps performance micro-optimisations. Certainly something to be conscious of though.Aryanize
There are no redeeming features to your snippets and I would not use any of them in my own projects. VincentSavard's flip&intersect_key technique is more performant, more concise, more elegant, more readable, and suitably utilizes a fully native functional approach. I am not attacking you, I am comparing the posts.Pitiable
E
10

Here is a more flexible solution using a closure:

$my_array = array("foo" => 1, "hello" => "world");
$allowed = array("foo", "bar");
$result = array_flip(array_filter(array_flip($my_array), function ($key) use ($allowed)
{
    return in_array($key, $allowed);
}));
var_dump($result);

Outputs:

array(1) {
  'foo' =>
  int(1)
}

So in the function, you can do other specific tests.

Ericson answered 25/1, 2013 at 10:38 Comment(5)
I wouldn't exactly call this "more flexible"; it feels a lot less straightforward than the accepted solution, too.Drennan
I agree. It would be more flexible is the condition was a more complex one.Ericson
Just passing by, for other users: This solution does not deal with the case that the $my_array has duplicate values or values that are not integers or strings. So I would not use this solution.Kanara
I agree this is more flexible as it allows you to change the filter logic. For example I used an array of disallowed keys and simply returned !in_array($key, $disallowed).Plasmasol
It is dangerous to call array_flip($my_array). If there are duplicate values in the array, the size of the array will be reduced because arrays cannot have duplicate keys in the same level. This approach should not be used -- it is unstable/unreliable.Pitiable
S
4

Here's a less flexible alternative using unset():

$array = array(
    1 => 'one',
    2 => 'two',
    3 => 'three'
);
$disallowed = array(1,3);
foreach($disallowed as $key){
    unset($array[$key]);
}

The result of print_r($array) being:

Array
(
    [2] => two
)

This is not applicable if you want to keep the filtered values for later use but tidier, if you're certain that you don't.

Seamount answered 31/10, 2012 at 3:50 Comment(3)
You should check if key $key exists in $array before doing unset.Magical
@JarekJakubowski you do not need to check if an array key exists when using unset(). No warnings are issued if the key doesn't exist.Safekeeping
I haven't benchmarked the viable solutions on this page, but this may be a contender for most performant.Pitiable
R
4

If you are looking for a method to filter an array by a string occurring in keys, you can use:

$mArray=array('foo'=>'bar','foo2'=>'bar2','fooToo'=>'bar3','baz'=>'nope');
$mSearch='foo';
$allowed=array_filter(
    array_keys($mArray),
    function($key) use ($mSearch){
        return stristr($key,$mSearch);
    });
$mResult=array_intersect_key($mArray,array_flip($allowed));

The result of print_r($mResult) is

Array ( [foo] => bar [foo2] => bar2 [fooToo] => bar3 )

An adaption of this answer that supports regular expressions

function array_preg_filter_keys($arr, $regexp) {
  $keys = array_keys($arr);
  $match = array_filter($keys, function($k) use($regexp) {
    return preg_match($regexp, $k) === 1;
  });
  return array_intersect_key($arr, array_flip($match));
}

$mArray = array('foo'=>'yes', 'foo2'=>'yes', 'FooToo'=>'yes', 'baz'=>'nope');

print_r(array_preg_filter_keys($mArray, "/^foo/i"));

Output

Array
(
    [foo] => yes
    [foo2] => yes
    [FooToo] => yes
)
Redcoat answered 28/2, 2014 at 16:42 Comment(4)
thanks for your answer. I would submit to you that using stristr within the "work" of the function is making some assumptions for the end user. Perhaps it would be better to allow the user to pass in a regular expression; this would give them more flexibility over certain things like anchors, word boundaries, and case sensitivity, etc.Drennan
I've added an adaptation of your answer that might help other peopleDrennan
You are certainly right, maček, that is a more versatile approach for users who are comfortable with regex. Thanks.Redcoat
This is the correct answer to a different question. Remove all elements from array that do not start with a certain string Your answer ignores the requirements in the asked question.Pitiable
F
4

How to get the current key of an array when using array_filter

Regardless of how I like Vincent's solution for Maček's problem, it doesn't actually use array_filter. If you came here from a search engine and where looking for a way to access the current iteration's key within array_filter's callback, you maybe where looking for something like this (PHP >= 5.3):

$my_array = ["foo" => 1, "hello" => "world"];

$allowed = ["foo", "bar"];

reset($my_array ); // Unnecessary in this case, as we just defined the array, but
                   // make sure your array is reset (see below for further explanation).

$my_array = array_filter($my_array, function($value) use (&$my_array, $allowed) {
  $key = key($my_array); // request key of current internal array pointer
  next($my_array); // advance internal array pointer

  return isset($allowed[$key]);
});

// $my_array now equals ['foo' => 1]

It passes the array you're filtering as a reference to the callback. As array_filter doesn't conventionally iterate over the array by increasing it's public internal pointer you have to advance it by yourself.

What's important here is that you need to make sure your array is reset, otherwise you might start right in the middle of it (because the internal array pointer was left there by some code of your's that was executed before).

Fourteen answered 23/1, 2015 at 10:2 Comment(6)
This answer completely ignores the asker's requirements and sample data. This answer is, at best, the correct answer to a different question ...except it's not. $&array is not valid PHP and each() has been deprecated since PHP7.2 and completely removed since PHP8.Pitiable
Hi @Pitiable and thank you for your kind and constructive words. Seven years ago, when I wrote this answer, PHP 8 wasn't even at the horizon and each() wasn't deprecated at all. Imho, the gist of my answer could be easily transferred to the asker's question but I updated it accordingly, so that now, it can be copied and pasted without the need to give it much thought. I also fixed the small typo with the references ($& => &$). Feel free to edit my answer if there's still something in it, you don't like. CheersFourteen
Please also keep in mind, that this question was called "How to use array_filter() to filter array keys?" (see: stackoverflow.com/posts/4260086/revisions) and was asked, when PHP 5.6 was not very widespread, so the new ARRAY_FILTER_USE_KEY flag wasn't commonly available. All answers on SO are children of their time and may not be valid, accurate or helpful more than half a decade later. I actually don't know if now-deprecated answers should be removed or kept for historical reasons. Someone might still be forced to support a project that uses a long-outdated version of PHP.Fourteen
Ask yourself, if you were a researcher who was looking for the "best" approach to implement in their application, would you consider this answer to be "worth reading"? Sometimes their is "academic value" in a posted answer despite it not being optimal. If you think your post will be helpful to future researchers, keep it here. If you think it adds unnecessary bloat to a page with 11 different answers, then spare researchers' time by trashing the post. Even decade-old pages need curation on SO, this is why I monitor new and old pages. I care more than the average user about our content.Pitiable
As a researcher I wouldn't have changed the title (and with that, possibly the subject of most of the answers) of a seven year old question. From a researchers point of view it would be very interesting to see, if there was a way "to use array_filter() to filter array keys?" seven years ago, when PHP 5.5 was installed on more than half of all systems. and there was no ARRAY_FILTER_USE_KEY flag around. There are many questions like this and in a way they pose a challenge: Is this even possible? As no answer actually used array_filter at the time, I'd say my answer still has value, yes.Fourteen
I think that deprecated answers should still be available. - for history and as well for people that may be still using OLD php (whatever language) - People that have used php 5.2 for example will know how to adapt the answer to a new version of php.Donetsk
H
4

Starting from PHP 5.6, you can use the ARRAY_FILTER_USE_KEY flag in array_filter:

$result = array_filter($my_array, function ($k) use ($allowed) {
    return in_array($k, $allowed);
}, ARRAY_FILTER_USE_KEY);


Otherwise, you can use this function (from TestDummy):

function filter_array_keys(array $array, $callback)
{
    $matchedKeys = array_filter(array_keys($array), $callback);

    return array_intersect_key($array, array_flip($matchedKeys));
}

$result = filter_array_keys($my_array, function ($k) use ($allowed) {
    return in_array($k, $allowed);
});


And here is an augmented version of mine, which accepts a callback or directly the keys:

function filter_array_keys(array $array, $keys)
{
    if (is_callable($keys)) {
        $keys = array_filter(array_keys($array), $keys);
    }

    return array_intersect_key($array, array_flip($keys));
}

// using a callback, like array_filter:
$result = filter_array_keys($my_array, function ($k) use ($allowed) {
    return in_array($k, $allowed);
});

// or, if you already have the keys:
$result = filter_array_keys($my_array, $allowed));


Last but not least, you may also use a simple foreach:

$result = [];
foreach ($my_array as $key => $value) {
    if (in_array($key, $allowed)) {
        $result[$key] = $value;
    }
}
Healall answered 29/8, 2015 at 20:46 Comment(2)
I fail to see any new value in this answer. It all seems over-engineered, convoluted, and/or redundant because earlier answers already offered direct approaches to solve the question asked. Please explain why your answer should stay on the page, if you can.Pitiable
When I posted this answer, PHP 5.6 had been released just 1 year ago, so it was far from being available on all hosts, hence the usefulness of userland implementations. Then, my answer isn't about providing a ready-to-pick (without really knowing what you do) solution. It's about thinking, step by step, how to solve the problem the best way. I'm expecting that once the reader has studied the answer, he should have understood the various approaches, and be able to determine how to solve the problem in his use case.Healall
H
4

array filter function from php:

array_filter ( $array, $callback_function, $flag )

$array - It is the input array

$callback_function - The callback function to use, If the callback function returns true, the current value from array is returned into the result array.

$flag - It is optional parameter, it will determine what arguments are sent to callback function. If this parameter empty then callback function will take array values as argument. If you want to send array key as argument then use $flag as ARRAY_FILTER_USE_KEY. If you want to send both keys and values you should use $flag as ARRAY_FILTER_USE_BOTH .

For Example : Consider simple array

$array = array("a"=>1, "b"=>2, "c"=>3, "d"=>4, "e"=>5);

If you want to filter array based on the array key, We need to use ARRAY_FILTER_USE_KEY as third parameter of array function array_filter.

$get_key_res = array_filter($array,"get_key",ARRAY_FILTER_USE_KEY );

If you want to filter array based on the array key and array value, We need to use ARRAY_FILTER_USE_BOTH as third parameter of array function array_filter.

$get_both = array_filter($array,"get_both",ARRAY_FILTER_USE_BOTH );

Sample Callback functions:

 function get_key($key)
 {
    if($key == 'a')
    {
        return true;
    } else {
        return false;
    }
}
function get_both($val,$key)
{
    if($key == 'a' && $val == 1)
    {
        return true;
    }   else {
        return false;
    }
}

It will output

Output of $get_key is :Array ( [a] => 1 ) 
Output of $get_both is :Array ( [a] => 1 ) 
Hurff answered 11/1, 2017 at 3:14 Comment(1)
This late answer has completely ignored the requirements of the question asked. This is, at best, thr correct answer to a different question.Pitiable
H
4

Based on @sepiariver I did some similar testing on PHP 8.0.3:

$arr = ['a' => 1, 'b' => 2, 'c' => 3, 'd' => 4, 'e' => 5, 'f' => 6, 'g' => 7, 'h' => 8];
$filter = ['a', 'e', 'h'];


$filtered = [];
$time = microtime(true);
$i = 1000000;
while($i) {
  $filtered = array_intersect_key($arr, array_flip($filter));
  $i--;
}
print_r($filtered);
echo microtime(true) - $time . " using array_intersect_key\n\n";


$filtered = [];
$time = microtime(true);
$i = 1000000;
while($i) {
  $filtered = array_filter(
    $arr,
    function ($key) use ($filter){return in_array($key, $filter);},
    ARRAY_FILTER_USE_KEY
  );
  $i--;
}
print_r($filtered);
echo microtime(true) - $time . " using array_filter\n\n";

$filtered = [];
$time = microtime(true);
$i = 1000000;
while($i) {
  foreach ($filter as $key)
    if(array_key_exists($key, $arr))
      $filtered[$key] = $arr[$key];
  $i--;
}
print_r($filtered);
echo microtime(true) - $time . " using foreach + array_key_exists\n\n";
  • 0.28603601455688 using array_intersect_key
  • 1.3096671104431 using array_filter
  • 0.19402384757996 using foreach + array_key_exists

The 'problem' of array_filter is that it will loop over all elements of $arr, whilst array_intersect_key and foreach only loop over $filter. The latter is more efficient, assuming $filter is smaller than $arr.

Hieroglyphic answered 11/6, 2021 at 8:13 Comment(3)
Why wasn't Alastair's snippet included in the benchmarks?Pitiable
Since, as he acknowledges himself, the functionality of his code is different than that of the three I've tested. In his case, $array ($arr in my code) is modified (unset); in my case $arr keeps its original state. Since functionality differ it's not fair to compare.Hieroglyphic
Well, if the original array needs to be preserved, then just save a copy before looping. Add that cost to the benchmark. Then the results will be the same.Pitiable
O
2

I use a small "Utils" class where I add two filter static function to filter array using a denylist or a allowlist.

<?php

class Utils {
 
  /**
   * Filter an array based on a allowlist of keys
   *
   * @param array $array
   * @param array $allowlist
   *
   * @return array
   */
  public static function array_keys_allowlist( array $array, array $allowlist ): array {
    return array_intersect_key( $array, array_flip( $allowlist ) );
  }


  /**
   * Filter an array based on a denylist of keys
   *
   * @param array $array
   * @param array $denylist
   *
   * @return array
   */
  public static function array_keys_denylist( array $array, array $denylist ): array {
    return array_diff_key($array,array_flip($denylist));
  }

}

You can then use it like this

<?php

$my_array = array("foo" => 1, "hello" => "world");
$allowed = array("foo", "bar");

$my_array = Utils::array_keys_allowlist($my_array,  $allowed)
Osteotome answered 15/9, 2022 at 22:34 Comment(2)
This answer merely restates what is already stated here and then answers a different question about how to do the inverse operation. Is it beneficial to write helper methods for what PHP already does well natively?Pitiable
@Pitiable the title of this question is broader than a deny list. It is beneficial to have such code centralized. You can then easily see performance impact because you have a function call and you can measure the resource usage of this function and do optimization in one place if needed.Osteotome
O
1

Perhaps an overkill if you need it just once, but you can use YaLinqo library* to filter collections (and perform any other transformations). This library allows peforming SQL-like queries on objects with fluent syntax. Its where function accepts a calback with two arguments: a value and a key. For example:

$filtered = from($array)
    ->where(function ($v, $k) use ($allowed) {
        return in_array($k, $allowed);
    })
    ->toArray();

(The where function returns an iterator, so if you only need to iterate with foreach over the resulting sequence once, ->toArray() can be removed.)

* developed by me

Ovariotomy answered 4/6, 2015 at 0:19 Comment(3)
in_array() is one of PHP's worst performing array searching functions. Adding the overhead of a library will only further slow down performance. Since two native functions or looped unset calls will concisely solve this problem, I would never entertain using a library's methods.Pitiable
@Pitiable While you're technically correct, it's a pure microptimization in most cases. If data with 10-100 items came from a database or a web service, you're making 0.5% of overall work, say, 5x faster, which achieves nothing. Of course, if we're talking about filtering 100,000 items coming right from RAM, then the overhead is considerable.Ovariotomy
Anyway, this is more of an example of the library having a straightforward conscise solution compared to clunky features introduced in PHP after the library was released (array_filter with ARRAY_FILTER_USE_KEY), using an unusual function no other language I know has (array_flip) or having deep knowledge of PHP architecture (knowing that unset has the speed of hash-table access and that in_array scales lineraly).Ovariotomy
D
0

Naive and ugly (but seems to be faster) solution?

Only tried this in php 7.3.11 but an ugly loop seems to execute in about a third of the time. Similar results on an array with a few hundred keys. Micro-optimization, probably not useful in RW, but found it surprising and interesting:

$time = microtime(true);
$i = 100000;
while($i) {
    $my_array = ['foo' => 1, 'hello' => 'world'];
    $allowed  = ['foo', 'bar'];
    $filtered = array_filter(
        $my_array,
        function ($key) use ($allowed) {
            return in_array($key, $allowed);
        },
        ARRAY_FILTER_USE_KEY
    );
    $i--;
}
print_r($filtered);
echo microtime(true) - $time . ' on array_filter';

// 0.40600109100342 on array_filter
$time2 = microtime(true);
$i2 = 100000;
while($i2) {
    $my_array2 = ['foo' => 1, 'hello' => 'world'];
    $allowed2  = ['foo', 'bar'];
    $filtered2 = [];
    foreach ($my_array2 as $k => $v) {
        if (in_array($k, $allowed2)) $filtered2[$k] = $v;
    }
    $i2--;
}
print_r($filtered2);
echo microtime(true) - $time2 . ' on ugly loop';
// 0.15677785873413 on ugly loop
Diaeresis answered 26/11, 2019 at 20:9 Comment(2)
Neither of these benchmarked filtering techniques should be used. There are better approaches that do not need in_array(). As the size of the whitelist array increases, the performance of in_array() will get ever worse.Pitiable
These were not posted as solutions to be used IRL, but just as illustrations that all else equal, sometimes a loop performs better than a native function. Replace in_array with whatever you like in both of those “benchmarks” and the results (relative performance not absolute) would be the same or similar.Diaeresis

© 2022 - 2024 — McMap. All rights reserved.