How can i merge items of a Laravel Collection based on keys? - php

I have a Laravel Collection with a lot of duplicated items like that:
[
id: 'NAME1',
prop1: 'yes',
prop2: null,
prop3: 'bla',
prop4: null
],
[
id: 'NAME1',
prop1: null,
prop2: 'yes'
prop3: null,
prop4: 'bla'
]
And i want to merge the elements with the same 'id' property, and get a collection like that, preserving both properties:
[
id: 'NAME1',
prop1: 'yes',
prop2: 'yes',
prop3: 'bla',
prop4: 'bla'
]
When i use $collection->unique('id') i only get a collection like that, losing the prop2 and prop4 of
the second element:
[
id: 'NAME1',
prop1: 'yes',
prop2: null,
prop3: 'bla',
prop4: null
]
How can i solve it? I doesn't find any method of Laravel Collections which could merge elements of a Collection when one of the elements had a null key.

Here's a macro that will do what you want:
use Illuminate\Support\Collection;
Collection::macro('mergeByKey', function ($key) {
return $this->groupBy($key)->map(function($group) {
$filteredGroup = collect($group)->map(function($item) {
return collect($item)->reject(function($value, $key) {
return $value === null;
});
});
return array_merge(...$filteredGroup->toArray());
})->values();
});
Then you can use it on a collection like this:
$collection = collect([
[
'id' => 'NAME1',
'prop1' => 'yes',
'prop2' => null,
'prop3' => 'bla',
'prop4' => null
],
[
'id' => 'NAME1',
'prop1' => null,
'prop2' => 'yes',
'prop3' => null,
'prop4' => 'bla'
],
[
'id' => 'NAME2',
'prop1' => null,
'prop2' => 'fdsa',
'prop3' => null,
'prop4' => 'asdf'
],
[
'id' => 'NAME2',
'prop1' => 'fdsa',
'prop2' => null,
'prop3' => 'asdf',
'prop4' => null
],
]);
$result = $collection->mergeByKey('id');
Result:
Collection {#268 ▼
#items: array:2 [▼
0 => array:5 [▼
"id" => "NAME1"
"prop1" => "yes"
"prop3" => "bla"
"prop2" => "yes"
"prop4" => "bla"
]
1 => array:5 [▼
"id" => "NAME2"
"prop2" => "fdsa"
"prop4" => "asdf"
"prop1" => "fdsa"
"prop3" => "asdf"
]
]
}

So you want to merge all non-null properties for each id (you have only one ID in your list, but I assume there can be many)
1) group by id and get the list of [id => [all property lists for id]]
2) for each id:
2a) remove empty properties from each list
2b) merge all lists into one
It can be done this way with laravel collections:
$data = '[
{
"id": "NAME1",
"prop1": "yes",
"prop2": null,
"prop3": "bla",
"prop4": null
},
{
"id": "NAME1",
"prop1": null,
"prop2": "yes",
"prop3": null,
"prop4": "bla"
},
{
"id": "NAME2",
"prop1": "no",
"prop2": "dah",
"prop4": "bla"
}
]
';
$coll = collect(json_decode($data, JSON_OBJECT_AS_ARRAY))
->groupBy('id')
->map(function ($propGroup) {
//for each group of 'objects' of property lists
return $propGroup
->map(function ($props) {
//remove empty properties
return collect($props)->filter(function ($prop) {
return !empty($prop);
});
})
->reduce(function ($carry, $item) {
return $carry->merge($item);
}, collect());
});

Related

Merge two multidimensional arrays using recursion, but not on full data set

I'm trying to add 2 array data to each other with array_merge(). It's just attached to the back. But lower levels are ignored.
Is there an alternative to array_merge() that will merge the user values without duolicating the color values?
Existing array data:
$existingtArr = [
"A" => [
"color" => 'red',
"user" => [
"Daniel" => ["01:18:08", "04:10:12"],
"Max" => ["01:04:00"],
"Serto" => ["02:00:02"],
]
],
"B" => [
"color" => 'blue',
"user" => [
"Franz" => ["08:40:52"],
"Hugo" => ["07:08:58"],
]
]
];
New array data:
$newArr = [
"A" => [
"color" => 'red',
"user" => [
"Fabian" => ["06:03:00"], // + 1 user
"Max" => ["04:10:12"], // + 1 new time
"Serto" => ["02:00:02"],
]
],
"B" => [
"color" => 'blue',
"user" => [
"Franz" => ["08:40:52", "14:05:32", "20:34:15"], // an older one is available, + 2 new times
"Hugo" => ["04:10:12"], // + 1 time
]
],
"C" => [ // + new whole group
"color" => 'green',
"user" => [
"Maxi" => ["07:08:58", "04:10:12"],
]
]
];
Supplement the existing data with the new data:
echo '<pre>';
print_r(array_merge($existingtArr, $newArr));
echo '</pre>';
Expected result array data:
$resultArr = [
"A" => [
"color" => 'red',
"user" => [
"Daniel" => ["01:18:08", "04:10:12"],
"Fabian" => ["06:03:00"],
"Max" => ["01:04:00", "04:10:12"],
"Serto" => ["02:00:02"],
]
],
"B" => [
"color" => 'blue',
"user" => [
"Franz" => ["08:40:52", "14:05:32", "20:34:15"],
"Hugo" => ["07:08:58", "04:10:12"],
]
],
"C" => [
"color" => 'green',
"user" => [
"Maxi" => ["07:08:58", "04:10:12"],
]
]
];
You cannot simply call array_merge_recursive() on the whole data sets because they will generated repeated color values, but you want the color values to remain singular and the user data to be recursively merged.
To accommodate this logic (assuming it is okay to simply mutate the $existingtArr array with the data from $newArr), perform a check for the existence of each letter-set, and either push the whole set for a non-present letter, or recursively merge the shared letter-sets.
Code: (Demo)
foreach ($newArr as $letter => $set) {
if (!isset($existingtArr[$letter])) {
$existingtArr[$letter] = $set;
} else {
$existingtArr[$letter]['user'] = array_merge_recursive(
$existingtArr[$letter]['user'],
$set['user']
);
}
}
var_export($existingtArr);

PHP - Get the key that has a value in multiple indexes

I have the below array:
$myArray = [
[
"name" => null,
"price" => [
"height" => 0.0098974902792506,
"left" => 0.8385,
"page" => 1,
"top" => 0.51290208554259,
"width" => 0.0275,
],
],
[
"name" => null
"price" => [
"height" => 0.0098974902792506,
"left" => 0.838,
"page" => 1,
"top" => 0.56981265464829,
"width" => 0.028,
]
],
[
"name" => null
"price" => [
"height" => 0.010250972074938,
"left" => 0.5905,
"page" => 1,
"top" => 0.44114528101803,
"width" => 0.0285,
]
]
];
I am trying to check the array and get the name of the key that has a value (is not null) in each array. In the above example, this would be price.
However, the array could also look like this:
[
[
"name" => null,
"price" => [
"height" => 0.0098974902792506,
"left" => 0.8385,
"page" => 1,
"top" => 0.51290208554259,
"width" => 0.0275,
],
],
[
"name" => null
"price" => null
],
[
"name" => null
"price" => null
]
]
In this case, there is not an array key that has a value in all of the arrays.
Below is my attempt to achieve this:
$originalKeyWithValue = null;
foreach($myArray as $key => $item)
{
$originalKeyWithValue = array_key_first($item);
if (isset($myArray[$key+1])) {
$nextKeyWithValue = array_key_first($myArray[$key+1]);
if($originalKeyWithValue != $nextKeyWithValue){
$originalKeyWithValue = $nextKeyWithValue;
}
}
}
return $originalKeyWithValue;
However the code above returns name as the key, even though it is null in all of the arrays in $myArray.
This is what would I do:
// I take first element of array as a source for indexes
foreach ($myArray[0] as $index => $item) {
// next I extract all elements from all subarrays under current `$index`
$values = array_column($myArray, $index);
// then I filter values to remove nulls.
// This also removes 0, empty arrays, false,
// so maybe you should change filter process
$values_filtered = array_filter($values);
// if number of filtered items is same as in original array - no nulls found
if (count($values_filtered) === count($values)) {
echo $index;
// optionally
// break;
}
}
Although there is an accepted answer, I thought I would share a way to do this using Laravel collections.
$uniqueKeysWithValues = collect($myArray)->map(function($item){
return array_keys( collect($item)->filter()->toArray() ); //filter will remove all null
})->flatten()->unique();
This approach will give you all keys that has values in it, even if there are values in both keys.

Laravel collection contains

I'm using the Laravel contains method on a collection https://laravel.com/docs/5.3/collections#method-contains. But it does not work for me.
foreach ($this->options as $option) {
if($options->contains($option->id)) {
dd('test');
}
}
dd($options); looks like this:
Collection {#390
#items: array:1 [
0 => array:3 [
0 => array:7 [
"id" => 10
"slug" => "test"
"name" => "test"
"poll_id" => 4
"created_at" => "2016-11-12 20:42:42"
"updated_at" => "2016-11-12 20:42:42"
"votes" => []
]
1 => array:7 [
"id" => 11
"slug" => "test-1"
"name" => "test"
"poll_id" => 4
"created_at" => "2016-11-12 20:42:42"
"updated_at" => "2016-11-12 20:42:42"
"votes" => []
]
2 => array:7 [
"id" => 12
"slug" => "test-2"
"name" => "test"
"poll_id" => 4
"created_at" => "2016-11-12 20:42:42"
"updated_at" => "2016-11-12 20:42:42"
"votes" => []
]
]
]
}
Result of dd($option->id); is 10.
What could be wrong? Or is there a better way?
You should pass a key / value pair to the contains method, which will
determine if the given pair exists in the collection. Use contains() method in this way:
foreach ($this->options as $option) {
// Pass key inside contains method
if($option->contains('id', $option->id)) {
dd('test');
}
}
Use the following, which tells Laravel you want to match the 'id':
$options->contains('id', $option->id);
Docs
foreach ($this->options as $option) {
if(!$options->flatten(1)->where('id',$option->id)->isEmpty()) {
dd('test');
}
}
The contains method determines whether the collection contains a given item.
There are basically three ways in which it can be used :
simply checking the item
$collection = collect(['name' => 'Sarah', 'age' => 23]);
$collection->contains('Desk');
// false
$collection->contains('Sarah');
// true
checking the key/value pair :
$collection = collect([
['name' => 'Sarah', 'age' => 23],
['name' => 'Vicky', 'age' => 34],
]);
$collection->contains('name', 'Hank');
// false
checking via callback function :
$collection = collect([3, 5, 7, 9, 11]);
$collection->contains(function ($value, $key) {
return $value < 2;
});
// false
Now, for your problem, we will use the second category, i.e :
foreach ($this->options as $option) {
// here id is key and $option->id is value
if($option->contains('id', $option->id)) {
dd('test');
}
}
Link to docs

How to Count the number of objects in elequont Object inside a group by in a already queried collection?

This is my current output
Collection {#794 ▼
#items: array:8 [▼
"IN" => Collection {#795 ▶}
"NZ" => Collection {#787 ▶}
]}
I want the items to be hold the no of count for each codes like
"IN" => 4,
"NZ" => 3,
I know that I can directly write in a query like this
$query->groupBy('country_code')->orderBy('country_code', 'ASC');
return $query->get([
DB::raw('country_code as country_code'),
DB::raw('COUNT(*) as "count"')
]);
But I want the output from a already queried collection to reduce multiple queries which is a collection.
Right now I am only able to group by on the collection like this
$collection->groupBy('country_code');
$b = $a->groupBy('country_code');
You've done most of the job by proper grouping the data based on the country_code. Now it's just to iterate through the collection with a foreach, key, value and use the collections's count() method to count the number of elements stored under a given country_code
foreach ($b as $countryCode => $items) {
echo $items->count()."\n";
}
Reproduce:
php artisan ti
Psy Shell v0.7.2 (PHP 7.0.8-0ubuntu0.16.04.3 — cli) by Justin Hileman
>>> $cities = collect([['country_code' => 'pl', 'name' => 'Warszawa'], ['country_code' => 'pl', 'name' => 'Wrocław'], ['country_code' => 'de', 'name' => 'Berlin']]);
=> Illuminate\Support\Collection {#846
all: [
[
"country_code" => "pl",
"name" => "Warszawa",
],
[
"country_code" => "pl",
"name" => "Wrocław",
],
[
"country_code" => "de",
"name" => "Berlin",
],
],
}
>>> $grouped = $cities->groupBy('country_code');
=> Illuminate\Support\Collection {#836
all: [
"pl" => Illuminate\Support\Collection {#838
all: [
[
"country_code" => "pl",
"name" => "Warszawa",
],
[
"country_code" => "pl",
"name" => "Wrocław",
],
],
},
"de" => Illuminate\Support\Collection {#837
all: [
[
"country_code" => "de",
"name" => "Berlin",
],
],
},
],
}
>>> foreach ($grouped as $cCode => $cities) {
... echo $cCode . ' has '.$cities->count()."\n";
... }
pl has 2
de has 1

Join more than one field using aggregate $lookup

I need to join more than two fields in two collections using aggregate $lookup. is it possible to join? please let me know if it is possible. Here i have two collections:
For Example:
"people" collections fields "city,state,country" in "country" collection fields "city_id,state_id,country_id", I want to join this three fields in following collections.
"People"
{
"_id" : 1,
"email" : "admin#gmail.com",
"userId" : "AD",
"userName" : "admin",
"city" : 1,
"state" : 1,
"country" : 1
}
"country"
{
"country_id" : 1,
"userId" : "AD",
"phone" : "0000000000",
"stateinfo":[{
"state_id" : 1,
"state_name" : "State1"
},{
"state_id" : 2,
"state_name" : "State2"
}
],
"cityinfo":[{
"city_id" : 1,
"city_name" : "city1"
},{
"city_id" : 2,
"city_name" : "city2"
}
]
}
This is probably a lot more simple than you think, considering that of course all of the "three" fields are contained within the one "country" document. So it's just a matter of doing the $lookup by "country_id" and then using the retrived content to populate the other fields.
var pipeline = [
{ "$lookup": {
"from": "country",
"localField": "country",
"foreignField": "country_id",
"as": "country"
}},
{ "$project": {
"email": 1,
"userId": 1,
"userName": 1,
"country": {
"$arrayElemAt": [
{ "$filter": {
"input": {
"$map": {
"input": "$country",
"as": "country",
"in": {
"country_id": "$$country.country_id",
"userId": "$$country.userId",
"phone": "$$country.phone",
"stateInfo": {
"$arrayElemAt": [
{ "$filter": {
"input": "$$country.stateInfo",
"as": "state",
"cond": { "$eq": [ "$$state.state_id", "$state" ] }
}},
0
]
},
"cityinfo": {
"$arrayElemAt": [
{ "$filter": {
"input": "$$country.cityinfo",
"as": "city",
"cond": { "$eq": [ "$$city.city_id", "$city" ] }
}},
0
]
}
}
}
},
"as": "country",
"cond": { "$eq": [ "$$country.userId", "$userId" ] }
}},
0
]
}
}}
]
db.people.aggregate(pipeline)
That should give you a result like:
{
"_id" : 1,
"email" : "admin#gmail.com",
"userId" : "AD",
"userName" : "admin",
"country" : {
"country_id" : 1,
"userId" : "AD",
"phone" : "0000000000",
"stateinfo": {
"state_id" : 1,
"state_name" : "State1"
},
"cityinfo": {
"city_id" : 1,
"city_name" : "city1"
}
}
So once the array is matched in by $lookup it all comes down to using $filter to do the matcing and $arrayElemAt to get the first match from each filtered array.
Since the outer array has "inner" arrays, you want to use $map for the "outer" source and apply $filter to each of it's "inner" arrays.
You can get more fancy with $let to get that "reduced" array content down to the returned sub-document and then just directly reference the resulting properties for an even "flatter" response, but the general concept of "matching" the array elements remains the same as above.
For a PHP structure translation:
$pipeline = array(
array(
'$lookup' => array(
'from' => 'country',
'localField' => 'country'
'foreignField' => 'country_id',
'as' => 'country'
)
)
array(
'$project' => array(
'email' => 1,
'userId' => 1,
'userName' => 1,
'country' => array(
'$arrayElemAt' => array(
array(
'$filter' => array(
'input' => array(
'$map' => array(
'input' => '$country',
'as' => 'country',
'in' => {
'country_id' => '$$country.country_id',
'userId' => '$$country.userId',
'phone' => '$$country.phone',
'stateInfo' => array(
'$arrayElemAt' => array(
array(
'$filter' => array(
'input' => '$$country.stateInfo',
'as' => 'state',
'cond' => array( '$eq' => array( '$$state.state_id', '$state' ) )
)
),
0
)
),
'cityinfo' => array(
'$arrayElemAt' => array(
array(
'$filter' => array(
'input' => '$$country.cityinfo',
'as' => 'city',
'cond' => array( '$eq' => array( '$$city.city_id', '$city' ) )
)
),
0
)
)
}
)
),
'as' => 'country',
'cond' => array( '$eq' => array( '$$country.userId', '$userId' ) )
)
),
0
)
)
)
)
);
$people->aggregate($pipeline);
You can usually check your PHP matches a JSON structure when you are working from a JSON example by dumping the pipeline structure:
echo json_encode($pipeline, JSON_PRETTY_PRINT)
And that way you cannot go wrong.
As another final note here, the process after the $lookup is done is quite "complex" even if very efficient. So I would advise that unless there is some need to take this aggregation pipeline further and actually "aggregate" something, then you are probably better off doing that "filtering" in client code rather than doing it on the server.
The client code to do the same thing is far less "obtuse" than what you need to tell the aggregation pipeline to do. So unless this "really" saves you a lot of bandwidth usage by reducing down the matched array, or indeed you if can just "lookup" by doing another query instead, then stick with doing it in code and/or do the seperate query.

Categories