I've got the following Laravel collection of between 800 and 4000 items:
[
{
"date": "2017-05-26",
"departure_time": "14:50:00",
"arrival_time": "09:20:02",
"departure_place_id": 16
},
{
"date": "2017-05-26",
"departure_time": "15:20:00",
"arrival_time": "15:20:00",
"departure_place_id": 15
},
...
]
I need to merge always merge two items of the collection into one. So that a collection of 10 items, after merge, is a collection of 5 items with the follwing structure.
[
{
"date": "2017-05-26",
"departure_time": "14:50:00", // from first item
"arrival_time": "15:20:00", // from second item
"departure_place_id": 16, // from first item
"arrival_place_id": 15 // from second item
},
...
]
As you can see I need to merge data from two consecutive items to one.
I already tried two extend use Laravels custom collection feature but cannot really achieve what I want.
Any clue?
You can solve it by using laravel collection chunk
public function formatCollection($collection)
{
$results = [];
foreach (collect($collection)->chunk(2) as $chunk)
{
$results[] = [
'date' => $chunk[0]['date'],
'departure_time' => $chunk[0]['departure_time'],
'arrival_time' => $chunk[1]['arrival_time'],
'departure_place_id'=> $chunk[0]['departure_place_id'],
'arrival_place_id' => $chunk[1]['departure_place_id']
];
}
return collect($results);
}
Call this function to format the data
IMO, if you only had two items, you should create a class which will "merge" your two items in one as you do.
Collection are made to treat a collection of items, so 1 to N items not only two.
After the tip from #Vikash with using chunk() and some research I came up with the following solution which suits my needs best:
public function convert() : \Illuminate\Support\Collection
{
/** #var \Illuminate\Database\Eloquent\Collection $collection */
$collection = $this->cruise->where('line_id', $lineId)->get();
return $collection->chunk(2)->map(function ($chunk) {
/** #var Collection $chunk */
return [
'date' => $chunk->first()->date,
'downriver' => $chunk->last()->departure_place_id < $this->departure_place_id,
'departure_time' => $chunk->first()->departure_time,
'arrival_time' => $chunk->last()->departure_time,
'departure_place_id' => $chunk->first()->departure_place_id,
'arrival_place_id' => $chunk->last()->departure_place_id,
];
});
}
I put that into a repository which is a nice way to decouple the logic. It utilizes the collection functions instead of dealing with arrays which I definately wanted to avoid.
Related
I am currently using CakePHP to serve a crud based api for some ticketing logic I wrote. I am running into an issue where I am attempting to change a belongsTo association and data within the new association and it is not persisting.
The controller doing the persisting looks like this:
<?php
class TasksController extends Cake\Controller\Controller
{
public function initialize()
{
parent::initialize();
}
public function edit(array $ids): void
{
$validationErrors = [];
$tasks = $this->Tasks->find('all')
->contain($this->setAssociations($query))
->where([$this->Model->getAlias().'.id IN' => $ids]);
foreach ($tasks as $id => $task) {
$this->Tasks->patchEntity($task, $this->request->getQuery()[$id], [
'associated' => ['Asset']
]);
}
if ($this->Tasks->saveMany($tasks)) {
$this->response = $this->response->withStatus(200);
} else {
// Handle errors
}
// Render json output for success / errors
$this->set($this->createViewVars(['entities' => $tasks], $validationErrors));
}
}
The association for an asset in the task table looks like this:
<?php
class TasksTable extends Cake\ORM\Table
{
public function initialize(array $config)
{
$this->belongsTo('Asset', [
'className' => 'Assets',
'foreignKey' => 'asset_id',
'joinType' => 'LEFT'
]);
}
}
These build rules are attached to the asset table:
<?php
class AssetsTable extends Cake\ORM\Table
{
public function buildRules(RulesChecker $rules)
{
$rules->add($rules->isUnique(['number', 'group_id'], 'The Number you selected is in use'));
$rules->add($rules->isUnique(['name', 'group_id'], 'The Name you selected is in use'));
}
}
The body of the request I am sending looks like this:
{
"421933": {
"description": "This task was edited by the api!",
"asset": {
"id": "138499",
"description": "This asset was edited by they api!",
"name": "105",
"number": "6"
}
}
}
Basically the name 105 and number 6 are being flagged as not being unique, because they are already set to those values on asset 138499. The query is is instead trying to edit name 105 and number 6 into the Asset entity that is presently associated with the Task entity (645163), which is triggering the isUnquie build rules to fail.
You can see this by printing the $tasks before the saveMany call in the above controller:
Array
(
[0] => App\Model\Entity\Task Object
(
[id] => 421933
[description] => This task was edited by the api!
.....
[asset] => App\Model\Entity\Asset Object
(
[id] => 645163
[name] => 105
[description] => This asset was edited by they api!
[number] => 6
....
)
)
)
It seems like this editing Asset 138499 as an association of Task 421933 should work as it is appears to be possible to save belongsTo associations in this fashion in the CakePHP docs, as documented here:
<?php
$data = [
'title' => 'First Post',
'user' => [
'id' => 1,
'username' => 'mark'
]
];
$articles = TableRegistry::getTableLocator()->get('Articles');
$article = $articles->newEntity($data, [
'associated' => ['Users']
]);
$articles->save($article);
Is it possible to associate a belongsTo association and edit it in the same transaction? If so how should my request or code be structured differently?
Thanks!
As hinted in the comments, you're running into a limitation of the marshaller there, for which there is no overly startightforward workaround yet (you might want to open an issue over at GitHub, maybe someone want's to take up the task of adding support for this type of marshalling/saving).
The example in the book isn't really the best to go by here, in fact I'd personally remove/change it, it would only work when mass assigning the primary key is allowed (which by default for baked entities it isn't), and it would also only work when creating new records, ie when the entity that is to be saved is marked as "new". However doing this would introduce all sorts of quirks, like validation rules would see the data as "create" (new), application rules would see the entity as "update" (not new), but not receive any of the existing/original data, same for other saving stage callbacks, which would mess up stuff like auditing or the like, basically anything where you need the new data and the original data.
Long story short, even if you could get it somewhat working like with that example, you'd just introduce other problems further down the road, so better forget about that for now. The only workaround-ish thing that comes to my mind right now, would be comparing the given asset primary key, loading the existing asset entity manually, and replacing the already loaded one before patching in your data, something along the lines of this:
foreach ($tasks as $task) {
$data = $this->request->getData($task->id);
$assetId = (int)\Cake\Utility\Hash::get($data, 'asset.id');
if ($task->asset->id !== $assetId) {
$task->asset = $this->Tasks->Asset->get($assetId);
}
$this->Tasks->patchEntity($task, $data, [
'associated' => ['Asset']
]);
}
Note that I've changed your getQuery() usage to getData(), as it seemed unlikely that you're passing that data via the query string.
I have data like this:
array:1 [
0 => "No Brand,ddfg"
]
First of all this data is wrong, what I want is to have something like this:
array:2 [
0 => "No Brand"
1 => "ddfg"
]
So now its really an array :)
Then I need my array data transform to lower case like:
array:2 [
0 => "no brand"
1 => "ddfg"
]
Code
$sibarBrandsArray = SidebarManager::first()->pluck('brands')->toArray();
This return data like:
array:1 [
0 => "No Brand,ddfg"
]
And this is how my data looks like in database:
Any idea?
Solved
// get my table row
$sibarBrandsArray = SidebarManager::first();
// get my row column
$getBrandColumn = $sibarBrandsArray->brands;
// separate data in that column with comma
$separateBrands = explode(',', $getBrandColumn);
// lowercase each separated data
$brandsArray = array_map('strtolower', $separateBrands);
// dump the result
dd($brandsArray);
Result
array:2 [
0 => "no brand"
1 => "ddfg"
]
Laravel has a very efficient and easy way to work with arrays. It's called a collection. Click here to learn more. Don't convert your response to the array, use collection directly.
$sibarBrandsCollection = SidebarManager::first()->pluck('brands');
Laravel eloquent by default gives you collection instance when you get a response. so pluck in above call is nothing but calling pluck on collection instance. We can chain method to the collection and do manipulation as needed.
$sibarBrandsCollection = $sibarBrandsCollection->map(function ($name) {
return strtolower($name);
});
Above code will automatically convert all of your values to lowercase. Similarly, you can explode the value to get your intended result. At last, if you have to send data as array to the browser just add toArray() method at the end of your collection.
I would not use core PHP array function unless needed, Laravel collection is great way to work with arrays.
$yourArray = array_map('strtolower', $yourArray);
$yourArray = array_map('nestedLowercase', $yourArray);
function nestedLowercase($value) {
if (is_array($value)) {
return array_map('nestedLowercase', $value);
}
return strtolower($value);
}
or you can use:
$query->whereRaw('LOWER(`newsTitle`) LIKE ? ',[trim(strtolower($newsTitle)).'%']);
In my HTML frontend, I have a jQuery DataTable displaying all records fetched via AJAX from the database - a rather pretty straight forward thing. I use the Laravel Collection's ->transform(function($o){ . . . }) to iterate through the collection and return it in an array-esque manner. Just think of the following piece of code in a controller:
$cAllRecords = DatabaseRecord::all();
if(!empty($aData['sFilterIds']))
{
$cAllRecords = $cAllRecords->whereIn('creator', explode(',', $aData['sFilterIds']));
}
return response()->json(['data' => $cAllRecords->transform(function ($oDatabaseRecord) {
/** #var $oDatabaseRecord DatabaseRecord */
$sActionsHtml = 'edit';
$sUrl = route('some.route', ['iDatabaseRecordId' => $oDatabaseRecord->getAttribute('od')]);
return [
$oDatabaseRecord->getAttribute('id'),
$oDatabaseRecord->getAttribute('updated_at')->toDateTimeString(),
$oDatabaseRecord->getAttribute('created_at')->toDateTimeString(),
$sActionsHtml
];
})]);
I'm actually just filtering for records created by certain user IDs (the whereIn() call in line 4. However, the response sent back to the client looks different for different users filtered leading the jQuery table to show 'no records available', as it had received an malformed answer from the server. For one user, the response looks like this:
{
"data":[
[
1,
"2019-05-29 16:44:53",
"2019-05-29 16:44:53",
"<a href=\"#\">edit<\/a>"
]
]
}
This is a correctly formed server response and will show up in the table regularly. Great! Now something that drives me insane - the same code for another user (ID 1, while the first request was for user ID 2) returns this:
{
"data":{
"1":[
3,
"2019-05-29 17:08:49",
"2019-05-29 17:08:49",
"<a href=\"#\">edit<\/a>"
]
}
}
which, pretty obviously, is malformed and is not correctly parsed by the datatable. OK, now combing them two filters and filtering for user ID 1 and 2 will, again, return the response correctly formatted:
{
"data":[
[
1,
"2019-05-29 16:44:53",
"2019-05-29 16:44:53",
"<a href=\"#\">edit<\/a>"
],
[
3,
"2019-05-29 17:08:49",
"2019-05-29 17:08:49",
"<a href=\"#\">edit<\/a>"
]
]
}
I tried a number of things, none of which had worked since it's merely guessing why it could work with one user and not with another. (Things like reversing the order of IDs to be filtered, etc., but I found out that the filtering is not the problem. It MUST be the transform, which behaves inconsistent.)
Any ideas on why this happens and how to tackle it? I mean, it's not the only way to achieve what I'm after, I was using ->each() and array_push for all the time before but wanted to get rid of it for the sake of making use of Laravel's helpers (or possibilites) - the manual iteration and array pushing process worked out seamlessly before, and even other parts of the app work well with the Collection transform over array iteration and pushing. Why doesn't it here?
Update: The ->map() collection method behaves exactly same. Map, as opposed by transform, does not alter the collection itself. However, this should not be a relevant part within this application any way. I really can't understand what's going wrong. Is this possibly Laravel's fault?
Please note that transform method returns a Illuminate\Support\Collection.
It's better that you call all() after the transform to get an array result.
Like this:
...
return response()->json(['data' => $cAllRecords->transform(function ($oDatabaseRecord) {
/** #var $oDatabaseRecord DatabaseRecord */
$sActionsHtml = 'edit';
$sUrl = route('some.route', ['iDatabaseRecordId' => $oDatabaseRecord->getAttribute('od')]);
return [
$oDatabaseRecord->getAttribute('id'),
$oDatabaseRecord->getAttribute('updated_at')->toDateTimeString(),
$oDatabaseRecord->getAttribute('created_at')->toDateTimeString(),
$sActionsHtml
];
})->all()]);
#Cvetan Mihaylov's answer made me look at all the available collection methods (https://laravel.com/docs/5.8/collections#available-methods) and I found ->values() to return the values reindexed. And - that did the trick! :-)
return response()->json(['data' => $cAllRecords->transform(function ($oDatabaseRecord) {
/** #var $oDatabaseRecord DatabaseRecord */
$sActionsHtml = 'edit';
$sUrl = route('some.route', ['iDatabaseRecordId' => $oDatabaseRecord->getAttribute('od')]);
return [
$oDatabaseRecord->getAttribute('id'),
$oDatabaseRecord->getAttribute('updated_at')->toDateTimeString(),
$oDatabaseRecord->getAttribute('created_at')->toDateTimeString(),
$sActionsHtml
];
})->values()]);
My Mongo collection has two documents
{
"_id":ObjectId("567168393d5c6cd46a00002a"),
"type":"SURVEY",
"description":"YOU HAVE AN UNANSWERED SURVEY.",
"user_to_notification_seen_status":[
{
"user_id":1,
"status":"UNSEEN",
"time_updated":1450272825
},
{
"user_id":2,
"status":"SEEN",
"time_updated":1450273798
},
{
"user_id":3,
"status":"UNSEEN",
"time_updated":1450272825
}
],
"feed_id":1,
"time_created":1450272825,
"time_updated":1450273798
}
Here is the query I used to fetch only if the user_id is 2 & status is "UNSEEN".
**$query = array('$and' => array(array('user_to_notification_seen_status.user_id'=> 2,'user_to_notification_seen_status.status' => "UNSEEN")));**
$cursor = $notification_collection->find($query);
Ideally the above query shouldn't retrieve results but it returning results. If I give an invalid id or invalid status, it is not returning any record.
You're misunderstanding how the query works. It matches your document because user_to_notification_seen_status contains elements with user_id: 2 and status: UNSEEN.
What you can do to get the desired results is use the aggregation framework; unwind the array and then match both conditions. That way you'll only get the unwinded documents with the array element satisfying both conditions.
Run this in mongo shell (or convert to PHP equivalent). Also, change YourCollection to your actual collection name:
db.YourCollection.aggregate([ { $unwind: "$user_to_notification_seen_status" }, { $match: { "user_to_notification_seen_status.status": "UNSEEN", "user_to_notification_seen_status.user_id": 2 } } ] );
This will return no records, but if you change the id to 3 for example, it will return one.
Try:
$query = array(
array('$unwind' => '$user_to_notification_seen_status'),
array(
'$match' => array('user_to_notification_seen_status.status' => 'UNSEEN', 'user_to_notification_seen_status.user_id' => 2),
),
);
$cursor = $notification_collection->aggregate($query);
I have a laravel collection object.
I want to use the nth model within it.
How do I access it?
Edit:
I cannot find a suitable method in the laravel documentation. I could iterate the collection in a foreach loop and break when the nth item is found:
foreach($collection as $key => $object)
{
if($key == $nth) {break;}
}
// $object is now the nth one
But this seems messy.
A cleaner way would be to perform the above loop once and create a simple array containing all the objects in the collection. But this seems like unnecessary duplication.
In the laravel collection class documentation, there is a fetch method but I think this fetches an object from the collection matching a primary key, rather than the nth one in the collection.
Seeing as Illuminate\Support\Collection implements ArrayAccess, you should be able to simply use square-bracket notation, ie
$collection[$nth]
This calls offsetGet internally which you can also use
$collection->offsetGet($nth)
and finally, you can use the get method which allows for an optional default value
$collection->get($nth)
// or
$collection->get($nth, 'some default value')
#Phil's answer doesn't quite obtain the nth element, since the keys may be unordered. If you've got an eloquent collection from a db query it'll work fine, but if your keys aren't sequential then you'll need to do something different.
$collection = collect([0 => 'bish', 2 => 'bash']); $collection[1] // Undefined index
Instead we can do $collection->values()[1] // string(4) bash
which uses array_values()
Or even make a macro to do this:
Collection::macro('nthElement', function($offset, $default = null) {
return $this->values()->get($offset, $default);
}):
Example macro usage:
$collection = collect([0 => 'bish', 2 => 'bash']);
$collection->nthElement(1) // string(4) 'bash'
$collection->nthElement(3) // undefined index
$collection->nthElement(3, 'bosh') // string (4) bosh
I am late to this question, but I thought this might be a useful solution for someone.
Collections have the slice method with the following parameters:
$items->slice(whereToStartSlice, sizeOfSlice);
Therefore, if you set the whereToStartSlice parameter at the nth item and the sizeOfSlice to 1 you retrieve the nth item.
Example:
$nthItem = $items->slice($nth,1);
If you are having problems with the collection keeping the indices after sorting... you can make a new collection out of the values of that collection and try accessing the newly indexed collection like you would expect:
e.g. Get the second highest priced item in a collection
$items = collect(
[
"1" => ["name" => "baseball", "price" => 5],
"2" => ["name"=> "bat", "price" => 15],
"3" => ["name" => "glove", "price" => 10]
]
);
collect($items->sortByDesc("price")->values())[1]["name"];
// Result: glove
Similar to morphs answer but not the same. Simply using values() after a sort will not give you the expected results because the indices remain coupled to each item.
Credit to #howtomakeaturn for this solution on the Laravel Github:
https://github.com/laravel/framework/issues/1335