31

I have a class called Collection which stores objects of same type. Collection implements array interfaces: Iterator, ArrayAccess, SeekableIterator, and Countable.

I'd like to pass a Collection object as the array argument to the array_map function. But this fails with the error

PHP Warning: array_map(): Argument #2 should be an array

Can I achieve this by implementing other/more interfaces, so that Collection objects are seen as arrays?

Mark Amery
  • 143,130
  • 81
  • 406
  • 459
f1ames
  • 1,714
  • 1
  • 19
  • 36

5 Answers5

32

The array_map() function doesn't support a Traversable as its array argument, so you would have to perform a conversion step:

array_map($fn, iterator_to_array($myCollection));

Besides iterating over the collection twice, it also yield an array that will not be used afterwards.

Another way is to write your own map function:

function map(callable $fn)
{
    $result = array();

    foreach ($this as $item) {
        $result[] = $fn($item);
    }

    return $result;
}

Update

Judging by your use-case it seems that you're not even interested in the result of the map operation; therefore it makes more sense to use iterator_apply().

iterator_apply($myCollection, function($obj) {
    $obj->method1();
    $obj->method2();

    return true;
});
Ja͢ck
  • 170,779
  • 38
  • 263
  • 309
  • 1
    This does work, but has a performance penalty because it will iterate during the iterator_to_array step and it will iterate again during the array_map step. – Eelke van den Bos Jan 30 '14 at 18:15
  • 2
    @EelkevandenBos I gave two solutions in my answer, the latter not exhibiting this "performance penalty"; besides that, in both cases the runtime is O(n). – Ja͢ck Jan 30 '14 at 23:26
  • I think the callback to iterator_apply does not receive the current instance as an argument. From the docs: "This function only receives the given args, so it is nullary by default." The example in the docs solves this by passing the iterator itself as an argument and using $iterator->current(). Docs: https://php.net/iterator_apply – Ole May 23 '19 at 11:18
  • @Ole thanks for that, it may have been an oversight when i authored my update – Ja͢ck May 23 '19 at 12:55
9

array_map wants, as the name suggests, arrays. It's not called iterator_map after all. ;)

Apart from iterator_to_array(), which produces a potentially large temporary array, there's no trick to make iterable objects work with array_map.

The Functional PHP library has a map implementation which works on any iterable collection.

Damian Yerrick
  • 4,602
  • 2
  • 26
  • 64
deceze
  • 510,633
  • 85
  • 743
  • 889
  • The Functional PHP map implementation is not memory efficient: the results are stored in array. I found a better library: https://github.com/SuRaMoN/itertools And a blog post explaining how you can build it yourself: http://www.a-basketful-of-papayas.net/2012/07/what-iterators-can-do-for-you.html – Aad Mathijssen Jul 27 '15 at 16:33
  • Aad, in general the result of a map function is a *new* array — the memory overhead is innate to the approach and is negligible in the vast majority of use cases. – Mark Fox Nov 01 '16 at 19:01
  • "There's no trick to make iterable objects work with `array_map`." That trick is `iterator_to_array()`. – Nathan Arthur May 12 '17 at 16:13
  • 1
    @MarkFox If the iterator yields a large collection of large arrays or objects, and the callable is intended to summarize each of them into a smaller array, object, or primitive, the memory overhead of first calling `iterator_to_array()` can be substantial. – Damian Yerrick Nov 10 '17 at 15:57
4

If you're not interested in creating a new array that is a function mapped over the original array, you could just use a foreach loop (because you implement Iterator).

foreach($item in $myCollection) {
    $item->method1();
    $item->method2();
}

if you actually want to use map, then I think you'll have to implement your own. I would suggest making it a method on Collection, eg:

$mutatedCollection = $myCollection->map(function($item) { 
    /* do some stuff to $item */
    return $item;
});

I would ask yourself if you really want to use map or do you really just mean foreach

Mark Amery
  • 143,130
  • 81
  • 406
  • 459
Chris M
  • 121
  • 1
  • 7
3

I came up with the following solution:

//lets say you have this iterator
$iterator = new ArrayIterator(array(1, 2, 3));

//and want to append the callback output to the following variable
$out = [];

//use iterator to apply the callback to every element of the iterator
iterator_apply(
    $iterator,
    function($iterator, &$out) {
        $current = $iterator->current();
        $out[] = $current*2;
        return true;
    },
    array($iterator, &$out) //arguments for the callback
);

print_r($out);

This way, you can generate an array without iterating twice as you would to with the approach like:

$iterator = new ArrayIterator(array(1,2,3));
$array = iterator_to_array($iterator); //first iteration
$output = array_map(function() {}, $array); //second iteration

Good luck!

Eelke van den Bos
  • 1,423
  • 1
  • 13
  • 18
1

I just stumbled upon this question and I managed to cast the collection to an array to make it work:

array_map($cb, (array) $collection);

disclaimer For the original question this might not be a suitable option but I found the question while looking to solve a problem which I solved with this solution. I would recommend using a custom iterator map where possible/viable.

another option is to do something like this:

foreach($collection as &$item) {
    $item = $cb($item);
}

which will mutate the underlying collection.

EDIT:

It has been pointed out that casting to an array can have unwanted side effects. It would be better to add a method to your collection to return the array from the iterator, and traverse that, or otherwise add a map method which accepts a callback and run a loop on the underlying iterator.

Andrew Willis
  • 2,289
  • 3
  • 26
  • 53
  • Blind casting to an array has the potential for nasty side-effects since you might end up with other data from the object in what gets iterated, instead of just getting the data that the iterator object is wrapping/navigating. – GuyPaddock Apr 26 '19 at 15:27
  • I agree with the principle, however since the object is a collection, the assumption being made was that there was no other data being bound to the object. – Andrew Willis Apr 27 '19 at 16:06
  • Given the interfaces implemented in the original question, it’s safe to assume that casting to an array would return an array representation of the iterator, however I have edited my answer to account for instances where people are trying to iterate over classes that do not implement the interfaces in OP – Andrew Willis Apr 27 '19 at 16:14