Cannot Unpack Array with String Keys

Cannot unpack array with string keys

The problem is that the splat operator (array unpacking operator or ...) does not work with associative arrays. Example:

$array = [1, 2, 3];
$assoc = ["one" => 1, "two" => 2, "three" => 3];

var_dump(...$array); //Works
var_dump(...$assoc); //Doesn't work

You need to force an array to be numerically indexed in order to unpack it. You do this using array_values:

$values = array_values($q->fetch());
ModelController::execute(...$values);

Array values will ensure all values have a sequential numeric key.

Update

Starting PHP 8 there will be support for named arguments which will make both cases work. An example is documented in the proposed RFC for named arguments which says the following code should work starting PHP 8

$params = ['start_index' => 0, 'num' => 100, 'value' => 50];
array_fill(...$params);

Cannot unpack array with string keys - passing multiple non-model data to mail view

The splat operator ... that you try to use on the line new $mailerClass(...$mailerClassParams) cannot work with an associative array like your $data

I can see that the constructor you use for your mailable class is public function __construct($data) so you should be able to just use new $mailerClass($mailerClassParams)

If you do have a mailable class with multiple parameters in the constructor such as public function __construct($email, $token, $name) then you can either still pass it as 1 array parameter and check the content of the array passed. or use new $mailerClass(...array_values($mailerClassParams)). However, take note that if you use end up using array_values() then the order of the array actually matters as that is how it will map the parameters so that first entry of $mailerClassParams array will always be the first parameter, as such this is not the recommended way.

function sendMailWithMailerClass($mailTo, $mailerClass, $mailerClassParams)
{
try{
// Remove ... splat operator here
Mail::to($mailTo)->send(new $mailerClass($mailerClassParams));
} catch (Exception $e) {

}
}

// make sure all your $mailClass constructors take 1 parameter
public function __construct($data)
{
$this->data = $data;
}

OR

function sendMailWithMailerClass($mailTo, $mailerClass, $mailerClassParams)
{
try{
// Remove keys of the associative array with array_values
Mail::to($mailTo)->send(new $mailerClass(...array_values($mailerClassParams)));
} catch (Exception $e) {

}
}

// make sure all constructor takes correct parameters in the correct order
public function __construct($email, $token, $name)
{
$this->data = [
'email' => $email,
'token' => $token,
'name' => $name,
];
}

Why the ...$args is adding value to sub-array? PHP

You are specifying that the function accept a variable number of arguments:

function withToken(...$arguments)

However, you pass ONE argument (an array). This doesn't expand into multiple arguments. You can attempt unpacking:

withToken(...[
'second' => 'second',
'third' => 'third'
]);

However you get:

Catchable fatal error: Cannot unpack array with string keys

This will work, but you won't get the string keys:

withToken(...[
'second',
'third'
]);

Same with this (no string keys):

call_user_func_array('withToken', [
'second' => 'second',
'third' => 'third'
]);

If you need string keys and values, stick with passing an array and not variable number of arguments.

Splatpacking versus array_values() to re-index an array with numeric keys

When re-indexing a 450,000 element array which has its first element unset...

array_values() is consistently twice as fast as splatpacking.

$array = range(0, 450000);
unset($array[0]);

Benchmark script

Sample output:

Duration of array_values: 15.328378677368
Duration of splat-pack: 29.623107910156

In terms of performance, you should always use array_values(). This is one case when a function-calling technique is more efficient than a non-function-calling technique.


I suppose the only scenario where the splatpacking technique wins is if you are a CodeGolfer -- 13 characters versus 5.

How to use a PHP associative array as function call arguments?

It turns out, I just had to use variadic function named param unpacking feature introduced in PHP 8 ( https://wiki.php.net/rfc/named_params#variadic_functions_and_argument_unpacking ) :

f(...$args); // output: 12

Prior to PHP 8, this code produces the error: Cannot unpack array with string keys.

Secondly, it turns out that call_user_func_array also works as expected in PHP 8 (see https://wiki.php.net/rfc/named_params#call_user_func_and_friends for explanation):

call_user_func_array('f', $args); // output: 12

- while it still outputs incorrect '21' in older versions.

Why Unpack function returns array with starting index 1 in PHP

The array is an associative array with named keys, not a regular array with numeric keys. The idea is that you will name each format code and the result array will use those names as the array keys.

For example:

<?php
$str = "PHP";
$binary_data = unpack("C*letter",$str);
print_r($binary_data);

Result:

Array
(
[letter1] => 80
[letter2] => 72
[letter3] => 80
)

From the PHP manual:

The unpacked data is stored in an associative array. To accomplish this you have to name the different format codes and separate them by a slash /. If a repeater argument is present, then each of the array keys will have a sequence number behind the given name.

Example #1 unpack() example

<?php
$binarydata = "\x04\x00\xa0\x00";
$array = unpack("cchars/nint", $binarydata);
?>

The resulting array will contain the entries "chars" with value 4 and "int" with 160.

Example #2 unpack() example with a repeater

<?php
$binarydata = "\x04\x00\xa0\x00";
$array = unpack("c2chars/nint", $binarydata);
?>

The resulting array will contain the entries "chars1", "chars2" and "int".

How detect if variable can be used with spread operator

The PHP Manual states that you only need to check if the data type is iterable.

Verify that the contents of a variable is accepted by the iterable
pseudo-type, i.e. that it is either an array or an object implementing
Traversable

is_iterable() is available from PHP7.1.

An empty array can still be spread, but this is equivalent to pass no arguments into your $function.

So, you can simplify the check to:

if (is_iterable($data) {
return $function(...$data);
}
return "Data is not iterable";

BUT, it is not enough to check is_iterable(). From PHP8 (it was proposed for PHP8.1, but is available in 8.0 -- see my demo link below), arrays with non-numeric keys can be spread. In PHP8, named parameters were introduced and this means that the spread operator can be used to unpack associative arrays with keys that match a function's argument signature.
In other words, if the function receiving this data is not designed to accommodate the spread-able data, then the script will fire a Fatal Error.

Code: (Demo)

function zeroOneTwo($one, $two, $three) {
return [$one, $two, $three];
}

function abc($b, $c, $a) {
return [$a, $b, $c];
}

function spread(...$data) {
return $data;
}

$numericKeyedArray = [4,5,6];
var_export(abc(...$numericKeyedArray));
echo "\n---\n";
var_export(spread(...$numericKeyedArray));
echo "\n===\n";
var_export(zeroOneTwo(...$numericKeyedArray));
echo "\n===\n";

$nonNumericKeyedArray = ['c' => 'sea', 'a' => 'A', 'b' => 'bee'];
echo "\n---\n";
var_export(abc(...$nonNumericKeyedArray));
echo "\n===\n";
var_export(spread(...$nonNumericKeyedArray));
echo "\n===\n";
var_export(zeroOneTwo(...$nonNumericKeyedArray));

Output (on PHP8):

array (
0 => 6,
1 => 4,
2 => 5,
)
---
array (
0 => 4,
1 => 5,
2 => 6,
)
===
array (
0 => 4,
1 => 5,
2 => 6,
)
===

---
array (
0 => 'A',
1 => 'bee',
2 => 'sea',
)
===
array (
'c' => 'sea',
'a' => 'A',
'b' => 'bee',
)
===

Fatal error: Uncaught Error: Unknown named parameter $c

This proves that you must not only check the type of the data being passed in, you must also acknowledge the context of what the called function does (specifically, what it can receive). If you can 100% guarantee that arrays will suit the function signature and/or that named parameters will never be used in function declarations, then you may be able to confidently go forward. Otherwise, there will remain the possibility of exposing your script to bugs. I am not sure if the toil involved in pre-checking the relationship of the data to the function call will be worth your intended gain in flexibility.

PHP - How to merge arrays inside array

array_merge can take variable number of arguments, so with a little call_user_func_array trickery you can pass your $result array to it:

$merged = call_user_func_array('array_merge', $result);

This basically run like if you would have typed:

$merged = array_merge($result[0], $result[1], .... $result[n]);

Update:

Now with 5.6, we have the ... operator to unpack arrays to arguments, so you can:

$merged = array_merge(...$result);

And have the same results. *

* The same results as long you have integer keys in the unpacked array, otherwise you'll get an E_RECOVERABLE_ERROR : type 4096 -- Cannot unpack array with string keys error.



Related Topics



Leave a reply



Submit