array_map on collection with array interfaces?
Asked Answered
C

5

32

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?

Catfall answered 29/4, 2013 at 7:23 Comment(2)
Roll your own collection_map function?Odie
@Odie Course I can, but now I'm looking for solution if I can use my Collection with buildin php funcs:)Catfall
P
33

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;
});
Pillage answered 29/4, 2013 at 8:33 Comment(4)
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.Marseilles
@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).Loom
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: php.net/iterator_applyKrigsman
@Krigsman thanks for that, it may have been an oversight when i authored my updateLoom
J
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.

Jamesjamesian answered 29/4, 2013 at 8:31 Comment(4)
The Functional PHP map implementation is not memory efficient: the results are stored in array. I found a better library: github.com/SuRaMoN/itertools And a blog post explaining how you can build it yourself: a-basketful-of-papayas.net/2012/07/…Classified
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.Brendon
"There's no trick to make iterable objects work with array_map." That trick is iterator_to_array().Astute
@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.Intersperse
G
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

Giselegisella answered 5/3, 2015 at 22:15 Comment(0)
M
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!

Marseilles answered 23/12, 2013 at 16:8 Comment(0)
P
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.

Peltz answered 16/9, 2018 at 19:13 Comment(3)
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.Delete
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.Peltz
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 OPPeltz

© 2022 - 2024 — McMap. All rights reserved.