Reputation: 24116
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:
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
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
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
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
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