Latheesan
Latheesan

Reputation: 24116

array_map and pass 2 arguments to the mapped function - array_map(): Argument #3 should be an array

I have an abstract class that looks like this:

abstract class Transformer {

    /**
     * Transform a collection of items
     *
     * @param array $items
     * @param bool $format
     * @return array
     */
    public function transformCollection(array $items, $format)
    {
        return array_map([$this, 'transform'], $items, $format);
    }

    /**
     * Transform a item
     *
     * @param array $item
     * @param bool $format
     * @return mixed
     */
    public abstract function transform(array $item, $format);

}

Then I have the following class that implements it:

class ServiceLogTransformer extends Transformer {

    public function transform(array $service_log, $format = false)
    {
        return [
            'id'    => $service_log['id'],
            'date'  => $service_log['log_date'],
            'time'  => $service_log['log_time'],
            'type'  => ($format ? status_label($service_log['log_type']) : $service_log['log_type']),
            'entry' => $service_log['log_entry']
        ];
    }

}

When this code runs, I get the error:

array_map(): Argument #3 should be an array

How do you pass 2 or more arguments when you call array_map function within a class? I checked the PHP Documentation and it looks like this is allowed, but it isn't working on my Larave 4.2 project.

Any ideas?

Upvotes: 33

Views: 47002

Answers (4)

Shaun Cockerill
Shaun Cockerill

Reputation: 926

Most answers show that an anonymous function with the use keyword is the typical way to pass additional arguments through to other callables.

abstract class Transformer {
    public function transformCollection(array $items, $format)
    {
        return array_map(function($item) use ($format) {
            return $this->transform($item, $format);
        }, $items);
    }
}

Most likely though, this particular situation would be better suited by a standard foreach loop over array_map, as it would potentially be more efficient and easier to read. This would also prevent your indexes from being renumbered, as well as work with Traversable and ArrayAccess items.

abstract class Transformer {
    public function transformCollection(array $items, $format)
    {
        foreach($items as $key => $item) {
           $items[$key] = $this->transform($item, $format);
        }
        return $items;
    }
}

If you really, really have your heart set on using array_map, anonymous functions don't work with your environment (i.e. pre PHP 5.3) and you need to pass $format through as the 2nd argument, then you will need to convert $format to an array with the same length as $items.

abstract class Transformer {
    public function transformCollection(array $items, $format)
    {
        // Fill an array of same length as $items with the $format argument.
        $format = array_fill(0, count($items), $format);
        return array_map([$this, 'transform'], $items, $format);
    }
}

EDIT:

I realised recently that there was another option available since you're using an instance to transform your data. It involves storing $format to the instance, possibly using a setter, and overriding it if it is provided as an argument. This way it's accessible via the transform method using $this->format.

abstract class Transformer {
    protected $format;

    public function setFormat($format)
    {
        $this->format = $format;
    }

    public function transformCollection(array $items, $format = null)
    {
        if (isset($format)) {
            $this->setFormat($format);
        }
        // ...
    }

    // ...
}

EDIT 2

Since PHP 7.4+, the shorthand/arrow function notation can be used to define an anonymous function that appears to be context aware, including the $this reference.

abstract class Transformer {
    public function transformCollection(array $items, $format)
    {
        return array_map(fn($item) => $this->transform($item, $format), $items);
    }
}

Please note that $format would be classed as a static variable, and $this refers to the object context when the function was defined, so storing your arrow function as a callable/closure variable may have unexpected results.

Upvotes: 9

imme
imme

Reputation: 645

This may not be applicable for laravel 4.2 // pre php 5.3 (as Shaun mentions) but might come in handy for some people who come across this question.

abstract class Transformer {

    /**
     * Transform a collection of items
     *
     * @param array $items
     * @param bool $format
     * @return array
     */
    public function transformCollection(array $items, $format)
    {
        $args = func_get_args();
        return $this->mapWith([$this, 'transform'], $args);
    }

    /**
     * @param callback<array|string> $callback
     * @param array $args first a collection to disect, then optional additional arguments to the callback
     * @return array
     */
    private function mapWith($callback, $args) {
        $data = array_shift($args);
        $closure = \Closure::fromCallable($callback);
        $scope = \is_array($callback) ? $callback[0] : null;
        return array_map(function ($item) use ($scope, $closure, $args) {
            array_unshift($args, $item);
            if (null !== $scope) {
                array_unshift($args, $scope);
                $closure = [$closure, 'call'];
            }
            return \call_user_func_array($closure, $args);
        }, $data);
    }

    /**
     * Transform a item
     *
     * @param array $item
     * @param bool $format
     * @return mixed
     */
    public abstract function transform(array $item, $format);

}

function status_label($index){return vsprintf('label: %s', [$index,]);}

#Then I have the following class that implements it:

class ServiceLogTransformer extends Transformer {

    public function transform(array $service_log, $format = false)
    {
        return [
            'id'    => $service_log['id'],
            'date'  => $service_log['log_date'],
            'time'  => $service_log['log_time'],
            'type'  => ($format ? status_label($service_log['log_type']) : $service_log['log_type']),
            'entry' => $service_log['log_entry']
        ];
    }

}


$logs = [
['id' => 123454, 'log_date'=>'20180926', 'log_time'=>'151913', 'log_type'=>'q', 'log_entry' => 'lorem',],
['id' => 353454, 'log_date'=>'20180926', 'log_time'=>'152013', 'log_type'=>'r', 'log_entry' => 'dolor',],
];

$slt = new ServiceLogTransformer();
$new = $slt->transformCollection($logs, false);
$lab = $slt->transformCollection($logs, true);
var_dump($logs);
var_dump($new);
var_dump($lab);

So, that's a dynamic use, by using the call-method on the Closure-class that's underneath php's anonymous functions. If the callback is an array, it will bind the scope of the ->call to the first array-element, which should be the method's object.

Upvotes: 2

Peter
Peter

Reputation: 16943

Please always read docs:

http://php.net/manual/en/function.array-map.php

array array_map ( callable $callback , array $array1 [, array $... ] )

and yet you pass bool $format as argument

"How do you pass 2 or more arguments when you call array_map function within a class?

I would create anonymous function with use() syntax

public function transformCollection(array $items, $format)
{
    return array_map(function($item) use ($format) {
        return $this->transform($item, $format);
    }, $items);
}

Upvotes: 73

alexrussell
alexrussell

Reputation: 14222

You can't use array_map to pass hard-coded values to a callback function (what is commonly referred to as currying or partially-applied functions in functional languages). What array_map takes is a variable amount of arrays, all of which should have the same amount of elements. The element at the current index of each array is passed to the callback as separate arguments. So, for instance, if you do this:

$arr1 = [1, 2, 3, 4];
$arr2 = [2, 4, 6, 8];

$func = function ($a, $b) { return $a.'-'.$b; };


$arr3 = array_map($func, $arr1, $arr2);

You get this:

['1-2', '2-4', '3-6', '4-8']

Hopefully that explains the idea behind it - each array you pass in will have the element at the current position in the first array passed as the relevant parameter to the callback function.

So, as I said, it's not to be used for passing 'static' values to the callback. However, you can do this yourself by defining an anonymous function on the fly. In your transformCollection method:

return array_map(function ($item) use ($format) {
    return $this->transform($item, $format);
}, $items);

Upvotes: 15

Related Questions