Why is array_merge_recursive not recursive? - php

I recently found a bug in my application caused by unexpected behaviour of array_merge_recursive. Let's take a look at this simple example:
$array1 = [
1 => [
1 => 100,
2 => 200,
],
2 => [
3 => 1000,
],
3 => [
1 => 500
]
];
$array2 = [
3 => [
1 => 500
]
];
array_merge_recursive($array1, $array2);
//returns: array:4 [ 0 => //...
I expected to get an array with 3 elements: keys 1, 2, and 3. However, the function returns an array with keys 0, 1, 2 and 3. So 4 elements, while I expected only 3. When I replace the numbers by their alphabetical equivalents (a, b, c) it returns an array with only 3 elements: a, b and c.
$array1 = [
'a' => [
1 => 100,
2 => 200,
],
'b' => [
3 => 1000,
],
'c' => [
1 => 500
]
];
$array2 = [
'c' => [
1 => 500
]
];
array_merge_recursive($array1, $array2);
//returns: array:3 [ 'a' => //...
This is (to me at least) unexpected behaviour, but at least it's documented:
http://php.net/manual/en/function.array-merge-recursive.php
If the input arrays have the same string keys, then the values for
these keys are merged together into an array, and this is done
recursively, so that if one of the values is an array itself, the
function will merge it with a corresponding entry in another array
too. If, however, the arrays have the same numeric key, the later
value will not overwrite the original value, but will be appended.
The documentation isn't very clear about what 'appended' means. It turns out that elements of $array1 with a numeric key will be treated as indexed elements, so they'll lose there current key: the returned array starts with 0. This will lead to strange outcome when using both numeric and string keys in an array, but let's not blame PHP if you're using a bad practice like that. In my case, the problem was solved by using array_replace_recursive instead, which did the expected trick. ('replace' in that function means replace if exist, append otherwise; naming functions is hard!)
Question 1: recursive or not?
But that's not were this question ends. I thought array_*_resursive would be a recursive function:
Recursion is a kind of function call in which a function calls itself.
Such functions are also called recursive functions. Structural
recursion is a method of problem solving where the solution to a
problem depends on solutions to smaller instances of the same problem.
It turns out it isn't. While $array1 and $array2 are associative arrays, both $array1['c'] and $array2['c'] from the example above are indexed arrays with one element: [1 => 500]. Let's merge them:
array_merge_recursive($array1['c'], $array2['c']);
//output: array:2 [0 => 500, 1 => 500]
This is expected output, because both arrays have a numeric key (1), so the second will be appended to the first. The new array starts with key 0. But let's get back to the very first example:
array_merge_recursive($array1, $array2);
// output:
// array:3 [
// "a" => array:2 [
// 1 => 100
// 2 => 200
// ]
// "b" => array:1 [
// 3 => 1000
// ]
// "c" => array:2 [
// 1 => 500 //<-- why not 0 => 500?
// 2 => 500
// ]
//]
$array2['c'][1] is appended to $array1['c'] but it has keys 1 and 2. Not 0 and 1 in the previous example. The main array and it's sub-arrays are treated differently when handling integer keys.
Question 2: String or integer key makes a big difference.
While writing this question, I found something else. It's getting more confusing when replacing the numeric key with a string key in a sub-array:
$array1 = [
'c' => [
'a' => 500
]
];
$array2 = [
'c' => [
'a' => 500
]
];
array_merge_recursive($array1, $array2);
// output:
// array:1 [
// "c" => array:1 [
// "a" => array:2 [
// 0 => 500
// 1 => 500
// ]
// ]
//]
So using a string key will cast (int) 500 into array(500), while using a integer key won't.
Can someone explain this behaviour?

If we take a step back and observe how array_merge*() functions behave with only one array then we get a glimpse into how it treats associative and indexed arrays differently:
$array1 = [
'k' => [
1 => 100,
2 => 200,
],
2 => [
3 => 1000,
],
'f' => 'gf',
3 => [
1 => 500
],
'99' => 'hi',
5 => 'g'
];
var_dump( array_merge_recursive( $array1 ) );
Output:
array(6) {
["k"]=>
array(2) {
[1]=>
int(100)
[2]=>
int(200)
}
[0]=>
array(1) {
[3]=>
int(1000)
}
["f"]=>
string(2) "gf"
[1]=>
array(1) {
[1]=>
int(500)
}
[2]=>
string(2) "hi"
[3]=>
string(1) "g"
}
As you can see, it took all numeric keys and ignored their actual value and gave them back to you in the sequence in which they were encountered. I would imagine that the function does this on purpose to maintain sanity (or efficiency) within the underlying C code.
Back to your two array example, it took the values of $array1, ordered them, and then appended $array2.
Whether or not this behavior is sane is a totally separate discussion...

You should read the link you provided it states (emphasis mine):
If the input arrays have the same string keys, then the values for these keys are merged together into an array, and this is done recursively, so that if one of the values is an array itself, the function will merge it with a corresponding entry in another array too. If, however, the arrays have the same numeric key, the later value will not overwrite the original value, but will be appended.

Related

Laravel array_diff attributes [duplicate]

This question already has answers here:
how to compare the values of 2 arrays with each other in php and output the differences [duplicate]
(2 answers)
Closed last year.
I'm trying to implement a RecordsActivity trait like in the Birdboard example on Laracasts. Here's Jeffrey's code updated for Laravel >=5.7:
/**
* Fetch the changes to the model.
*
* #return array|null
*/
protected function activityChanges()
{
if ($this->wasChanged()) {
// weirdness here, see below
dump(
$this->oldAttributes,
$this->getAttributes(),
array_diff($this->oldAttributes, $this->getAttributes())
);
return [
'before' => Arr::except(array_diff($this->oldAttributes, $this->getAttributes()), 'updated_at'),
'after' => Arr::except($this->getChanges(), 'updated_at')
];
}
}
The dump() above produces the below response during the test (without annotations). Nothing changes between the two arrays, and it's obvious that one row is different, but somehow array_diff is failing.
// $this->oldAttributes
array:14 [
"name" => "890 Gleichner Lights Suite 446"
"address" => "890 Gleichner Lights Suite 446"
"city" => "Mantetown"
"state" => "CT"
"postal_code" => "00627"
"active" => false
]
// $this->getAttributes()
array:14 [
"name" => "890 Gleichner Lights Suite 446"
"address" => "Changed"
"city" => "Mantetown"
"state" => "CT"
"postal_code" => "00627"
"active" => false
]
// array_diff($this->oldAttributes, $this->getAttributes())
[]
Check the array_diff
array_diff ( array $array1 , array $array2 [, array $... ] ) :array
Compares array1 against one or more other arrays and returns the values in array1 that are not present in any of the other arrays.
So if one field's value is set empty, change the order cannot work too, for example: set the "address" => "", then array_diff($this->getAttributes(), $this->oldAttributes) will return [] too.
array_diff_assoc ( array $array1 , array $array2 [, array $... ] ) : array
Compares array1 against array2 and returns the difference. Unlike array_diff() the array keys are also used in the comparison.
Use array_diff_assoc is better:
$changes = array_diff_assoc($this->oldAttributes(), $this->getAttributes);

PHP - MongoDB: Retrieve Fields from Sub Document with Field Names in Array

Consider an Array
$lettersArray = [A,C,E,G]
and my MongoDB Collection has the following structure.
{
Collection : {
letters:{
A:{...},
B:{...},
...
Z:{...}
}
}
}
Consider that the Letter Sub document is a part of a larger collection. Hence I am using Aggregation.
Right Now I have tried to project -
['$Collection.letters' => ['$elemMatch' => ['$in' => $lettersArray]]
and also tried
['Letters' => ['$in' => [$lettersArray,'$Collection.letters']]
But it didn't work.
In the End, I want result like the following:
[
Collection => [
letters => [
A => [...],
C => [...],
E => [...],
G => [...]
]
]
]
Is there any way to do this?
In PHP you can use array_combine with array_fill to create the empty arrays.
$lettersArray = ['A','C','E','G'];
$letters = array_combine($lettersArray, array_fill(0,count($lettersArray), []));
Array_fill creates an array from index 0, to the count of items in $lettersArray with the contents []
Output:
array(4) {
["A"]=>
array(0) {
}
["C"]=>
array(0) {
}
["E"]=>
array(0) {
}
["G"]=>
array(0) {
}
}
https://3v4l.org/TeoFv
I think you are mistaken in the way you are trying to access the documents' information.
If you take a look at your MongoDB document, you will see that it is in fact not an array, so you should not use $elemMatch to project these fields, but simple projections. In your case, you should project in this way:
[
'$project' => [
'Collection.letters.A' => 1,
'Collection.letters.C' => 1,
'Collection.letters.E' => 1,
'Collection.letters.G' => 1
]
]
By the way, you don't need to use the aggregation framework to compute such a projection. You could just use find(), and use the projection in the options, which are the functions' second argument:
myCollection->find(query, [
'projection' => [
'Collection.letters.A' => 1,
'Collection.letters.C' => 1,
'Collection.letters.E' => 1,
'Collection.letters.G' => 1
]
]);
Cheers,
Charles

php, array_merge_recursive works well with string keys only

$array1 = [
'1' => '11',
'b' => 1,
3 => 33,
8 => 8
];
$array2 = [
'1' => '22',
'b' => 2,
3 => 44,
9 => 9
];
$merged = array_merge_recursive($array1, $array2);
and the result is:
array(7) {
[0]=>
string(2) "11"
["b"]=>
array(2) {
[0]=>
int(1)
[1]=>
int(2)
}
[1]=>
int(33)
[2]=>
int(8)
[3]=>
string(2) "22"
[4]=>
int(44)
[5]=>
int(9)
}
so lets take a glance: the only part is the 'b' keys, they are actually works. I dont want to overwrite anything of it but putting them together to an array. Thats good! But then keys the other numeric keys (int or string) are screwed up.
I want to have this as result:
[
'1' => ['11', '22']
'b' => [1, 2]
[3] => [33, 44]
[8] => 8,
[9] => 9
]
possible?
EDIT: of course keys "1" and 1 - string vs int key are the same
Let's break down this question into to separate problems:
When a key in the second array exist in the first array, you want to create an array and make the value the first element of that array.
To be honest, I don't know an easy way of solving this. I'm not sure there is one. And even if, I'm not sure you really want it. Such a function will lead to arrays having values that are a string or an array. How would you handle such an array?
Update: Well, there is one. Your example already shows that array_merge_recursive will convert values with a string key into an array. So 1 => 'foo' would be 0 => 'foo', but 'foo' => 'bar' will end up as 'foo' => ['bar']. I really don't understand this behaviour.
Using string keys would help you out in this case, but after learning more about array_merge_recursive I decided to avoid this function when possible. After I asked this question someone filed it as a bug in it since PHP 7.0, since it works differently in PHP 5.x.
You want to keep the keys, while array_merge_resursive doesn't preserve integer keys, while it does for integer keys:
If the input arrays have the same string keys, then the values for
these keys are merged together into an array, and this is done
recursively, so that if one of the values is an array itself, the
function will merge it with a corresponding entry in another array
too. If, however, the arrays have the same numeric key, the later
value will not overwrite the original value, but will be appended.
To make it worse, it handles differently when handling the nested arrays:
$array1 = [30 => [500 => 'foo', 600 => 'bar']];
$array2 = [];
array_merge_recursive($array1, $array2);
//[0 => [500=> 'foo', 600 => 'bar']];
So effectively, array_merge_resursive isn't really resursive.
Using array_replace_resursive solves that problem:
array_replace_recursive($array1, $array2);
//output:
//array:5 [
// 1 => "22"
// "b" => 2
// 3 => 44
// 8 => 8
// 9 => 9
//]
Please note that PHP is very consistent in being inconsistent. While array_merge_recursive isn't recursive, array_replace_recursive doesn't replace (it also appends):
If the key exists in the second array, and not the first, it will be
created in the first array.
In many cases this is desired behavior and since naming functions isn't PHP's strongest point, you can consider this as a minor issue.
Can you rely on a native function to return your exact desired output? No. At least not in any version as of the date of this post (upto PHP8.1).
The good news is that crafting your own solution is very simple.
Code: (Demo)
foreach ($array2 as $key => $value) {
if (!key_exists($key, $array1)) {
$array1[$key] = $value;
} else {
$array1[$key] = (array)$array1[$key];
$array1[$key][] = $value;
}
}
var_export($array1);
I suppose I am less inclined to recommend this output structure because you have potentially different datatypes on a given level. If you were building subsequent code to iterate this data, you'd need to write conditions on every level to see if the data was iterable -- it just feels like you are setting yourself up for code bloat/convolution. I'd prefer a result which has consistent depth and datatypes.

Unsetting element in array

I am trying to remove a key/value pair from an array but it does not seem to work. Basically, I make an API call which returns JSON. As such I do
$tempArray = json_decode($projects, true);
If I output $tempArray I see something like this
array:2 [
0 => array:9 [
"id" => 4
"name" => "Some Project Name"
"value" => "234"
"user_id" => "1"
"client_id" => "97"
"contact" => "Jane Berry"
]
1 => array:9 [
"id" => 3
"name" => "Another Project Name"
"value" => "6"
"user_id" => "1"
"client_id" => "97"
"contact" => "John Doe"
]
]
I essentially need to remove the value element so I do this
unset($tempArray['value']);
If I output $tempArray after the unset, it displays exactly the same as before, with the value element and value there.
What do I need to do to completely remove this from my array?
Thanks
unset will not look recursivly to sub-array to remove the key value. Only if you have at first level a key named value will be removed. In your array first level keys are: 0 and 1.
So to remove value from all sub-arrays you have to go throw all items from the array and unset it. You can do this with a simple foreach.
foreach($tempArray as $key => $data) {
unset($data['value']);
$tempArray[$key] = $data; //Overwrite old data with new with value unset.
}
Now you will not have value key in sub-array items.
As per my comment, you have no key called 'value' which is a top level key in your array. If you array looked like this:
$myArray = array(
"value" => "My Value to delete",
"anotherKey" => "hello world",
);
Then you could do unset($myArray['value']); and you would remove the key and value. In your case, the key you are looking for is nested under a numeric key [0] or [1]. You could reference these specifically like this:
unset($tempArray[0]['value']);
but what I imagine you are looking to achieve is to remove any trace of the key value from your array in which case you would be better off doing something like this:
foreach($tempArray as &$nestedArray){
unset($nestedArray['value']);
}
Note the & symbol before the $nestedArray. This means 'pass by value' and will actually update the $tempArray in a single line without the need for anything else.
Further Reading:
PHP Docs - Arrays
PHP Docs - Foreach loop
PHP Docs - Pass by reference

Why is this array count returning 1 instead of 2?

Why is this array count returning 1 instead of 2?
Shouldn't it return 2?
$join = [
'JOIN' => ['coins','users.id','user_id'],
'JOIN' => ['coins','users.id','user_id']
];
echo count($join);
You are creating an associative array, which means that each element is associated to one unique key. Therefore, in an array, each key can only appear once. A key appearing twice means that the value will be overwritten.
If you try to var_dump your array, it would have this output:
array(1) {
["JOIN"]=>
array(3) {
[0]=>
string(5) "coins"
[1]=>
string(8) "users.id"
[2]=>
string(7) "user_id"
}
}
As seen from this result, only one line exists.
If you need to have 'JOIN' in every element, maybe you want to change your array structure into this:
$join = [
['JOIN' => ['coins','users.id','user_id']],
['JOIN' => ['coins','users.id','user_id']]
];
This will carry the information 'JOIN' in every element. However, I cannot imagine why you would need such a thing.
Instead, maybe you want to have multiple elements under the 'JOIN key:
$join = [
'JOIN' => [
['coins','users.id','user_id'],
['coins','users.id','user_id']
]
];
As per your comments, maybe you eventually want to have a structure like this:
$join = [
'JOIN' => [
['coins','users.id','user_id'],
['coins','users.id','user_id'],
],
'INNER JOIN' => [
['coins','users.id','user_id'],
['coins','users.id','user_id'],
]
];
According to your comments, it might be more desirable if you do this through object-oriented programming instead:
class Join{
const JOIN = 0;
const INNER_JOIN = 1;
// we are using constants to prevent bugs caused by typos
public $type;
public $coins;
public $usersDotId; // I don't really know what you are trying to do here
public $userId;
}
Then you can use it like this:
$joins = [];
$join = new Join();
$join->type = Join::INNER_JOIN;
$join->coins = "some_value";
$join->usersDotId = "some_value";
$join->userId = "some_value";
$joins[] = $id;
$join = [
'JOIN' => ['coins','users.id','user_id'],
'JOIN' => ['coins','users.id','user_id']
];
echo count($join);
how may I modify this array to have 2 keys, without changing the key name?
In order to not need to change this key name, make JOIN an array of numeric values so structurally you want:
array:
----> [Join] :
\---> [0]
\---> coins
|---> users.id
|---> user.id
This can be achieved with this syntax (clarified for understanding):
$join = [
'JOIN' => [0] => ['coins','users.id','user_id'],
[1] => ['coins','users.id','user_id']
];
(simplified for ease):
$join = [
'JOIN' => ['coins','users.id','user_id'],
['coins','users.id','user_id']
];
gives you a $join array with a count of 1, which contains (the one) an array with a count of 2, each of those containing 3 elements.
Without addressing the more specific problem that appeared in the comments, and acknowledging that you've already accepted an answer that works for your purposes, here is a more theoretical explanation for why you can't have two of the same key. This may be not be useful for you, but hopefully it could help someone who does not intuitively grasp this concept.
Regardless of whether you assign your own string keys ($array = ['key' => 'value'];), assign your own integer keys ($array = [42 => 'the answer'];), or let PHP automatically assign integer keys ($array[] = 'something';), the keys must be unique.
According to the PHP manual for arrays:
An array in PHP is actually an ordered map. A map is a type that associates values to keys.
Most programming languages have something like this. This association of values to keys means that by definition, keys must be unique. If a language allowed you to create a "map" with multiple identical keys pointing to different values, looking up a value by key would be impossible (or at least would produce ambiguous results), so the map would be fairly useless.
PHP will let you write things like:
$example = [
'a' => 1,
'b' => 2,
'a' => 3,
];
var_dump($example);
without giving any errors, but the result of that will be:
array (size=2)
'a' => int 3
'b' => int 2
where 'a' => 3 has overwritten the previously defined value of the 'a' key. This allows the map to continue to work, so that $example['a'] will always yield the same value. If you actually could have
array (size=3)
'a' => int 1
'b' => int 2
'a' => int 3
Then what would be the result of $example['a']? Any defined behavior (first instance of 'a', etc.) would mean that some values of 'a' would be inaccessible.
Because you are using same array key to both the element
try this,
<?php
$join = [
'JOIN1' => ['coins','users.id','user_id'],
'JOIN2' => ['coins','users.id','user_id']
];
echo count($join);
?>
Or with same key try following
$join = [
['JOIN' => ['coins','users.id','user_id']],
['JOIN' => ['coins','users.id','user_id']]
];

Categories