I have a Laravel collection like this (approximating using array syntax; the actual data is a Collection of objects obtained from an API response, not a local DB):
$rows = [
[
'id': 1,
'name': 'Sue',
'age': 23,
],
[
'id': 2,
'name': 'Joe',
'age': 25,
],
]
I want to extract a subset of the fields:
$subset = [];
foreach ($rows as $row) {
$subset[] = ['name' => $row['name'], 'age' => $row['age']];
}
So that I end up with:
$subset = [
[
'name': 'Sue',
'age': 23,
],
[
'name': 'Joe',
'age': 25,
],
]
What Collection method should I use to achieve that instead of the for loop?
I found this suggestion, using a higher-order message, which made some kind of sense:
$subset = $rows->map->only(['name', 'age']);
but that just gives me a Collection of null values. Expanding it into a conventional map call produced the same effect. I feel like I want some kind of multipluck, but I'm not sure what that corresponds to!
Update
It turns out that I was doing this correctly with the higher-order map->only approach. However, while the items in my collection were a kind of Model, they were not a subclass or compatible implementation of the Laravel Model class, and lacked an implementation of the only method. The author added the method, and now it works as expected.
You were close, but you don't chain map and only, and only doesn't seem to work on a Collection of nested arrays/objects.
So, for your case, use map() with a Callback:
$rows = collect([
(object)[
'id' => 1,
'name' => 'Sue',
'age' => 23,
],
(object)[
'id' => 2,
'name' => 'Joe',
'age' => 25,
]
]);
$mapped = $rows->map(function ($row) {
return ['age' => $row->age, 'name' => $row->name];
});
dd($mapped->toArray());
Output of that would be:
array:2 [▼
0 => array:2 [▼
"age" => 23
"name" => "Sue"
]
1 => array:2 [▼
"age" => 25
"name" => "Joe"
]
]
Note: If these are arrays and not objects, then you'd do $row['age'] and $row['name'] instead of $row->age and $row->name. In Laravel, Models are both, and allow either syntax.
References:
https://laravel.com/docs/9.x/collections#method-map
https://laravel.com/docs/9.x/collections#method-only
Edit:
Some alternatives. If you have a Collection of Models, then you can natively do:
$mapped = $rows->map(function ($model) {
return $model->only(['age', 'name']);
});
If you have a Collection of Collections, then you can do:
$mapped = $rows->map(function ($collection) {
return $collection->only(['age', 'name']);
});
And lastly, if you arrays or objects, you can collect() and call ->only():
$mapped = $rows->map(function ($row) {
return collect($row)->only(['age', 'name']);
});
Related
I have a collection which contains these values
'sales marketing|telemarketing',
what I'm trying to do is query/filter the items in collection but just based on the individual type so the for example value of 'telemarketing'. I have tried
$results = $data->where('Department', 'contains', $type); and also tried LIKE but because of the format with the pipe it's not picking the type/value.
This might be a dumb question but any ideas would be great!
The where-method also can handle only two Parameters. For example:
$data= collect([
['Department' => 'sales', 'price' => 200],
['Department' => 'marketing', 'price' => 100],
['Department' => 'telemarketing', 'price' => 150],
['Department' => 'marketing', 'price' => 100],
]);
$departmentName = "marketing";
$results = $data->where('Department', $departmentName);
dd($results);
Given your example:
[
"Employee" => "Some Company",
"Name" => "John Something",
"Usages" => "sales marketing|telemarketing",
"StartDate" => "1st Mar 2021",
"EndDate" => ""
]
The main issue is that the "Usage" property is a string containing multiple values, with the pipe character acting as a separator.
One solution to filter by one of those values is by mapping your original collection to transform the string in an array with the explode method and then use the filter method to filter based on the Usages you're interested in.
The resulting code might look like this:
$mappedCollection = $collection->map(function($el) {
$el['Usages'] = explode('|', $el['Usages']); // Transform the string into an array
return $el;
});
$result = $mappedCollection->filter(function($el) {
return in_array('sales marketing',$el['Usages']); // Change 'sales marketing' with the desired Usage
});
I am using Laravel .if i dd() $arrBody I get following result dd($arrBody);
array:7[▼
"first_name" => "john"
"last_name" => "doe"
"email_address" => "john#gmail.com",
"age"=> 25,
"date"=>"2021-07-9",
"country"=>"USA",
"code"=>"3045"
]
Now I want to get email,firstname and last name from $arryBody and assigned them to email,first_name and last_name key. But rest of keys like age, country,state and date i want them to go in custom_fields array but its like a hardcoded here. since I am displaying age, date,country one by one. My array may have more key/values so I want to make custom_fields array dynamic. I want field_name inside inside custom_fields to have a same name that comes after email,first_name and last_name of $arrBody array instead of manually writing field_name and want to assign that keys value to "value"
$data = [
"subscribers"=>[
[
"email"=> $arrBody['email'],
"first_name"=> $arrBody['first_name'],
"last_name"=> $arrBody['last_name'],
"subscribed"=> true,
"custom_fields"=>[
[
"field_name"=> "age",
"value_type"=> "text",
"value"=> array_key_exists('age',$arrBody) ? $arrBody['age']:''
],
[
"field_name"=> "country",
"value_type"=> "text",
"value"=> array_key_exists('country',$arrBody) ? $arrBody['country']:''
],
[
"field_name"=> "date",
"value_type"=> "text",
"value"=> array_key_exists('date',$arrBody) ? $arrBody['date']:''
],
//so on...
]
]
]
];
In Laravel I can use
$main = ['email', 'first_name', 'last_name'];
$subscriber = Arr::only($arrBody, $main);
$custom = Arr::exclude($arrBody, $main);
Now I want this $custom array inside "custom_fields"=>[] dynamically until the length of $custom array instead of checking if it has age, country etc or not.
something like this if possible
custom_fields" => [
[
"field_name" => array_keys($custom)
"value_type" => "text",
"value" => array_values($custom)
],
//go until the end of $custom array
];
$data = [
"subscribers"=>[
[
"email"=> $arrBody['email'],
"first_name"=> $arrBody['first_name'],
"last_name"=> $arrBody['last_name'],
"subscribed"=> true,
"custom_fields"=> collect($custom)->map(function($value, $key) {
return [
'field_name' => $key,
'value_type' => gettype($value),
'value' => $value,
];
}),
]
]
];
add ->toArray() after if you want it back to an array over a collection,
Also gettype wont work unless you actually use those types, not a number in a string format.
I am looking for a way to sort the collection in such a way that name values starting with the alphabet comes at the top and then name values that start with numbers. For example:
$collection = collect([
['name' => 'b', 'symbol' => '#'],
['name' => '2a', 'symbol' => '$'],
['name' => '1', 'symbol' => '#'],
['name' => 'a', 'symbol' => '%']
]);
The above collection should be sorted like this:
[
[
"name" => "a",
"symbol" => "%",
],
[
"name" => "b",
"symbol" => "#",
],
[
"name" => "1",
"symbol" => "#",
],
[
"name" => "2a",
"symbol" => "$",
],
]
But this is what I get when I sort it using sortBy method:
$collection->sortBy('name')->values()->all();
[
[
"name" => "1",
"symbol" => "#",
],
[
"name" => "2a",
"symbol" => "$",
],
[
"name" => "a",
"symbol" => "%",
],
[
"name" => "b",
"symbol" => "#",
],
]
Any idea how to sort this collection so that names starting with letters come first?
You need to define your own custom comparator function to sort these collection objects using sort.
Compare both names by checking they are all alphabets. If both are alphabets, then usual string comparison using strcasecmp shall suffice. If either of them is an alphabet, push them to higher ranks by returning value -1, meaning to be placed above in the sorted order. If both are numerical or alphanumeric, use strcasecmp again.
<?php
$collection = collect([
['name' => 'b', 'symbol' => '#'],
['name' => '2a', 'symbol' => '$'],
['name' => '1', 'symbol' => '#'],
['name' => 'a', 'symbol' => '%']
]);
$collection = $collection->sort(function($a,$b){
$a_is_alphabet = preg_match('/^[a-zA-Z]+$/', $a['name']) === 1;
$b_is_alphabet = preg_match('/^[a-zA-Z]+$/', $b['name']) === 1;
if($a_is_alphabet && $b_is_alphabet){
return strcasecmp($a['name'], $b['name']);
}elseif($a_is_alphabet){
return -1;
}elseif($b_is_alphabet){
return 1;
}
return strcasecmp($a['name'], $b['name']);
});
You want purely alphabetical name values to have top priority, then I assume natural sorting so that, say a2 comes before a10. Just write two rules in a custom callback in a sort() method call.
False evaluations are ordered before true evaluations when sorting ASC, so merely write the $b element before the $a element to sort DESC. To break any ties on the first comparison, call strnatcmp().
Laravel adopted arrow function syntax back in 2019.
Code: (Basic PHP Demo)
$collection->sort(fn($a, $b) =>
(ctype_alpha($b['name']) <=> ctype_alpha($a['name']))
?: strnatcmp($a['name'], $b['name'])
);
If you, more specifically only want to check if the first character is a letter, you can use $a['name'][0] and $b['name'][0]. If the strings might have a multi-byte first character then a regex approach might be best.
I am using the PHP MongoDB\Driver\Manager and I want to query by creating a MongoDB\Driver\Query.
So I have the following collection design:
{
"_comment": "Board",
"_id": "3",
"player": "42",
"moves": [{
"_id": "1",
"piece": "b3rw4",
"from_pos": "E2",
"to_pos": "E4"
}]
}
How can i query this collection to receive, for all boards of a specific player all moves with min(id)? This means I first want to filter all boards, to get only boards with player ID. Then I want to search all those board's "moves" fields, where I want the min(_id) of that "moves" field.
I currently have this query:
$filter = ['player' => '93'];
$options = [
'projection' => ['_id' => 0,
'moves' => 1]
];
$query = new MongoDB\Driver\Query($filter, $options);
This results in finding all "moves" arrays by Player 93.
How can I then filter all those "moves" fields by only getting the moves with min(_id)?
Ok, so I figured it out. I simply had to use an aggregation pipeline.
Here is the shell command which gives the expected output:
db.boards.aggregate( [
{
$match: {'player': '93'}
},
{
$unwind: {path: '$moves'}
},
{
$group:
{
_id: '$_id',
first_move: { $min: '$moves._id' },
from_pos : { $first: '$moves.from_pos' },
to_pos: { $first: '$moves.to_pos' }
}
}
])
Here is the corresponding PHP MongoDB code using Command and aggregate:
$command = new MongoDB\Driver\Command([
'aggregate' => 'boards',
'pipeline' => [
['$match' => ['player' => '93']],
['$unwind' => '$moves'],
['$group' => ['_id' => '$_id',
'firstMove' => ['$min' => '$moves._id'],
'from_pos' => ['$first' => '$moves.from_pos'],
'to_pos' => ['$first' => '$moves.to_pos']
]
]
],
'cursor' => new stdClass,
]);
$manager = new MongoDB\Driver\Manager($url);
$cursor = $manager->executeCommand('db', $command);
PHP re-group array by each column to multiple array without loop
In Laravel DB return, i.e. ->get()->toArray() result is as such:
[
['ts' => 1234, 'a' => 3, 'b' => 2],
['ts' => 1244, 'a' => 2, 'b' => 6],
['ts' => 1254, 'a' => 8, 'b' => 3],
]
Is there any way that I am able to transform the data to as such:
[
['column' => 'a', 'values' => [[1234, 3], [1244, 2], [1254, 8]],
['column' => 'b', 'values' => [[1234, 2], [1244, 6], [1254, 3]],
]
I just wish to know if there's any best / efficient way to render the transformation as described above. Avoid re looping again as data is already available, it can be thought as a data formatting question. Also, I do not wish to use additional array if possible.
Things that I've already looked includes ->get()->map(function()), however this is sequential and I am not able to get values in 2D array as a result.
You'll want to map over your items and return the ts value, as well as the value for each column, respectively:
$items = Model::all();
$result = [
[
'column' => 'a',
'values' => $items->map(function ($item) {
return [$item->ts, $item->a];
})
],
[
'column' => 'b',
'values' => $items->map(function ($item) {
return [$item->ts, $item->b];
})
],
];
If you want to combine the logic for both columns, create a collection of column names, and map over those:
$items = Model::all();
$result = collect(['a', 'b'])->map(function ($column) use ($items) {
$values = $items->map(function ($item) use ($column) {
return [$item->ts, $item->{$column}];
});
return compact('column', 'values');
});