Laravel, how to sort and paginate of loop array - php

I have this code in controller, so I need to paginate and sort by using distance, I dont know how to do this, Im new to laravel , thanks in advance
$stores = [];
foreach (session('storeinfo') as $storeInfo) {
$store = Storeinfo::find($storeInfo['id']);
if ($store) {
$store->distance = $storeInfo['distance'];
$stores[] = $store;
$stores = collect([]);
if (!Collection::hasMacro('paginate')) {
Collection::macro('paginate', function ($perPage = 25, $page = null, $options = []) {
$options['path'] = $options['path'] ?? request()->path();
$page = $page ?: (Paginator::resolveCurrentPage() ?: 1);
return new LengthAwarePaginator(
$this->forPage($page, $perPage)->values(),
$this->count(),
$perPage,
$page,
$options
);
});
}
}
}
return view('stores.archive',compact('stores'));
Im placing id into session using this:
$allstores= Storeinfo::all();
foreach ($allstores as $allstore) {
Session::push('storeinfo', [
'id' => $allstore->id,
'distance' => $miles
]);
}}
where $mile comes from calculation of distance
enter code here

First, I would create a collection instance of your $stores array:
$stores = collect([]);
I prefer using the push() api of the collection to add items:
$stores->push($store);
Second, the collection instance doesn't provide a native way to paginate so you need to add a macro:
use Illuminate\Support\Collection;
use Illuminate\Pagination\Paginator;
use Illuminate\Pagination\LengthAwarePaginator;
...
if (!Collection::hasMacro('paginate')) {
Collection::macro('paginate', function ($perPage = 25, $page = null, $options = []) {
$options['path'] = $options['path'] ?? request()->path();
$page = $page ?: (Paginator::resolveCurrentPage() ?: 1);
return new LengthAwarePaginator(
$this->forPage($page, $perPage)->values(),
$this->count(),
$perPage,
$page,
$options
);
});
}
I personally added the above macro in AppServiceProvider so that I can use it anywhere in my project. This should be located in the app/Providers directory.
To paginate you simply need to call the paginate() macro you just created:
$stores->paginate(15);
If you wish to set the current page or path, you may do so:
$stores->paginate(15, 1, ['path' => 'your/custom/path']);
To sort, all you need to do is used the desired sortBy method to achieve your results.
Per the docs, sortBy takes a string:
$sorted = $collection->sortBy('price');
Or a callback:
$sorted = $collection->sortBy(function ($product, $key) {
return count($product['colors']);
});
The method, sortByDesc() works the same way as sortBy().

Related

Eloquent - Fetch, merge and paginate data from multiple models

I'm working on an application where users have different types of events. Each event type has its own database table/Laravel model. Right now I'm performing multiple database queries to fetch the events from the tables. After that I merge them manually by using for-each loops and creating a uniform structure.
Because the code is really long, I give you guys an example code here:
$output = [];
$events1 = EventType1::where('user',$user_id)->get();
foreach ($events1 as $ev1) {
$output[] = [
"id" => $ev1->id,
"date" => $ev1->id,
"attribute3" => $ev1->attributeA
];
}
$events2 = EventType2::where('user',$user_id)->get();
foreach ($events2 as $ev2) {
$output[] = [
"id" => $ev2->id,
"date" => $ev2->id,
"attribute3" => $ev2->someOtherAttribute
];
}
// More fetches here....
// ...
// ...
usort($output, function ($a, $b) {
return strcmp($a["date"], $b["date"]);
});
return $output;
So right now I want to improve the performance by using Pagination. But the way I fetch the data, I don't think it will work?!
Can someone help me how to fetch, merge and paginate all the events the proper way?
Is there a way to union all the data using Eloquent?
Thanks
If you want with pagination you can follow this. I have used one of my projects for merging different model data as well as including pagination. If you don't need pagination just ignore pagination method calling part.
use Illuminate\Pagination\LengthAwarePaginator;
use Illuminate\Pagination\Paginator;
use Illuminate\Support\Collection;
public function events(){
$events1 = EventType1::where('user',$user_id)->get();
$events2 = EventType2::where('user',$user_id)->get();
$collection = new Collection();
$collection = $collection->merge($events1);
$collection = $collection->merge($events2);
$merge = $this->paginate($collection, $limit);
return $merge;
}
public function paginate($items, $perPage = 10, $page = null, $options = [])
{
$page = $page ?: (Paginator::resolveCurrentPage() ?: 1);
$items = $items instanceof Collection ? $items : Collection::make($items);
return new LengthAwarePaginator($items->forPage($page, $perPage), $items->count(), $perPage, $page, $options);
}

Laravel flatten won't flatten array error

In my Laravel project I'm trying to flatten an array to ensure consistency, for some reason, the attached screenshot showing my data format returned from my project won't flatten with the flatten() method.
I get an error:
Error: Call to a member function flatten() on array
Which is quite generic, I've tried using ->toArray() before flattening but this doesn't give me any data, what am I doing wrong here?
The logic exists within a Laravel job, thus the console log
/**
* Group data
*
* #return void
*/
public function groupData(
$data,
$groupBy,
$groupByFormat,
$additionFromField = ''
) {
$results = $data->groupBy(function ($item, $key) use ($groupBy, $groupByFormat) {
$date = Carbon::parse($item->{$groupBy});
return $date->format($groupByFormat);
});
// grouping by some kind of total
if (!empty($additionFromField)) {
$results = $results->map(function ($item, $key) use ($additionFromField) {
$totals = 0;
foreach ($item as $key => $value) {
$totals += $value->{$additionFromField};
}
return [
'items' => count($item),
'total' => $totals ?? 0
];
});
$calcedData = [];
foreach ($results as $key => $result) {
array_push($calcedData, [
'period_to' => $key,
'items' => $result['items'],
'total' => $result['total']
]);
}
return $calcedData;
}
// standard grouping of data
$results = $results->map(function ($item, $key) {
return $item[0];
});
return $results;
}
/**
* Execute the job.
*
* #return void
*/
public function handle()
{
$filters = json_decode($this->report->discovery_filters, true);
$data = [];
foreach ($filters as $findableKey => $findable) {
/*
** If there are datasets on the findable objec, then we assume
** that we can build up a chart or some data structure.
*/
if (isset($findable['datasets'])) {
$pushableDatasets = [];
foreach ($findable['datasets'] as $datasetKey => $dataset) {
// query data
if (isset($dataset['query'])) {
$additionFromField = $dataset['query']['additionFromField'] ?? '';
$res = DB::table($dataset['query']['table'])
->select($dataset['query']['columns'])
->where($dataset['query']['filterBy'])
->orderBy($dataset['query']['orderBy']['field'], $dataset['query']['orderBy']['direction'])
->get()
->chunk(100);
$res = $res->flatten();
if (isset($dataset['query']['useGrouping']) && $dataset['query']['useGrouping'] == 'yes') {
$results = $this->groupData(
$res,
$dataset['query']['groupBy'],
$dataset['query']['groupByFormat'],
$additionFromField
);
var_dump($results); // shown in the screenshot
$resultData = $results->flatten();
array_push($pushableDatasets, $this->getStructure($findable, $datasetKey, $resultData));
}
}
}
$findable['datasets'] = $pushableDatasets;
}
array_push($data, $findable);
}
}
Error: Call to a member function flatten() on array
The error message is quite accurate and descriptive. flatten is a member function of an object (in this case, the Laravel Collection object) and an array is not an object.
You need to convert the array to a collection first, then you can flatten it:
$flattened = collect($results)->flatten();
$new = collect($results)->flatten();
In your case you can also use array_flatten() I guess. Here is the documentation https://laravel.com/docs/5.1/helpers#method-array-flatten

Laravel modify Collection data

I wonder if Laravel have any helper to modify a collection.
What I need to do is to make a query with paginate() then check if the logged in users ID match the sender or receiver and based on that add a new value to the output:
$userId = Auth::guard('api')->user()->user_id;
$allMessages = Conversation::join('users as sender', 'conversations.sender_id', '=', 'sender.user_id')
->join('users as reciver', 'conversations.recipient_id', '=', 'reciver.user_id')
->where('sender_id',$userId)->orWhere('recipient_id',$userId)
->orderBy('last_updated', 'desc')
->select('subject','sender_id','recipient_id', 'sender_unread', 'recipient_unread', 'last_updated', 'reciver.username as receivername', 'sender.username as sendername')
->paginate(20);
Now I want to do something like:
if ($allMessages->sender_id == $userId) {
// add new value to output
newField = $allMessages->sendername
} else {
// add new value to output
newField = $allMessages->receivername
}
Then send the data with the new value added
return response()->json(['messages' => $allMessages], 200);
Is this possible?
You're better off using the Collection class's built-in functions for this. For example, the map function would be perfect.
https://laravel.com/docs/5.3/collections#method-map
$allMessages = $allMessages->map(function ($message, $key) use($userId) {
if ($message->sender_id == $userId) {
$message->display_name = $message->receivername;
} else {
$message->display_name = $message->sendername;
}
return $message;
});
Solved by adding:
foreach ($allMessages as $message) {
if ($message->sender_id == $userId) {
$message->display_name = $message->receivername;
} else {
$message->display_name = $message->sendername;
}
}
You can surely use the laravel's LengthAwarePaginator.
Along with total count of collection you also need to pass the slice of collection's data that needs to be displayed on each page.
$total_count = $allMessages->count();
$per_page = 2;
$current_page = request()->get('page') ?? 1;
$options = [
'path' => request()->url(),
'query' => request()->query(),
];
Suppose you want 2 results per page then calculate the offset first
$offset = ($current_page - 1) * $per_page;
Now slice the collection to get per page data
$per_page_data = $collection->slice($offset, $per_page);
$paginated_data = new LengthAwarePaginator($per_page_data, $total_count, $per_page, $current_page, $options);
$paginated_data will have only limited number of items declared by $per_page variable.
If you want next two slice of data then pass api_request?page="2" as your url.
As I don't know which Laravel version you're using, taking Laravel 5.2 let me give you a smarter way to deal with this (if I get your problem correctly).
You can use Laravel's LengthAwarePaginatior(API Docs).
Don't use paginate method when you are bulding your query, instead of that use simple get method to get simple collection.
$userId = Auth::guard('api')->user()->user_id;
$allMessages = Conversation::join('users as sender', 'conversations.sender_id', '=', 'sender.user_id')
->join('users as reciver', 'conversations.recipient_id', '=', 'reciver.user_id')
->where('sender_id',$userId)->orWhere('recipient_id',$userId)
->orderBy('last_updated', 'desc')
->select('subject','sender_id','recipient_id','sender_unread','recipient_unread','last_updated','reciver.username as receivername','sender.username as sendername')
->get();
Now you can populate extra items into that collection based on your certain conditions like this.
if ($allMessages->sender_id == $userId ) {
// add new value to collection
} else {
// add new value to collection
}
Now use LengthAwarePaginator, to convert that populated collection into a paginated collection.
$total_count = $allMessages->count();
$limit = 20;
$current_page = request()->get('page');
$options = [
'path' => request()->url(),
'query' => request()->query(),
];
$paginated_collection = new LengthAwarePaginator($allMessages, $total_count, $limit, $current_page, $options);
The variable $paginated_collection now can be used to be sent in response. Hope this helps you to deal with your problem.

Laravel Remove [data] from collection

I've tried to query using eloquent and fractal
$lists = Category::all();
$result = Fractal::collection($lists, new CategoryTransformer())->getArray();
and return it
return response()->json((['code' => "200", 'results' => $result]));
the json result is this:
{"code":"200","results":{"data":[{"id":"1","name":"Cafe","logo":null,"cover":""},{"id":"2","name":"SPA","logo":null,"cover":""},{"id":"3","name":"Hotel","logo":null,"cover":""}]}}
How to remove "data" after result?. So i can just get the array without "data".
I've tried:
$result = Fractal::collection($lists, new CategoryTransformer(), 'results')->getArray();
return (['code' => "200", $result]);
it return me :
{"code":"200","0":{"results":[{"id":"1","name":"Cafe","logo":"","cover":""},{"id":"2","name":"SPA","logo":"","cover":""},{"id":"3","name":"Hotel","logo":"","cover":""}]}}
There is leading '0' before results. how can i remove it?
Thanks
Try this:
return (['code' => "200", "results" => $result['results']);
I think the array method can't deal with a given array.
An other solution would be to add your results:
$result['code'] = 200;
return $result;
The data is just the key, I think it won't make any issues. If you still need to remove it, update getArray() function.
Put these Collection Macros in your AppServiceProvider::boot() method:
/**
* Remove the unnecessary nested 'data' keys
*
* #param string $case For consistency, define the type of keys that should be returned
*/
Collection::macro('fractal', function ($case = 'snake_case') {
//Handle this as a nested function to block access to the $depth flag.
//It's purpose is to indicate how deep the recursion is, and,
//more importantly, when it's handling the top-level instance
$recursion = function ($case = 'snake_case', array $items = [], $depth = 0) use (&$recursion) {
//If the array has only one element in it, and it's keyed off 'data', remove the wrapper.
//However, if it has a sibling element, such as 'meta', leave it alone
if (array_key_exists('data', $items) && count($items) == 1) {
$items = $items['data'];
}
$items = (new static($items))->mapWithKeys_v2(function ($item, $key) use (
$case,
$recursion,
$depth
) {
$key = $case ? $case($key) : $key;
//If the nested item is itself an array, recursively perform the same transformation
return is_array($item) ?
[$key => $recursion($case, $item, ++$depth)] : [$key => $item];
})->toArray();
//Maintain the top-level 'data' wrapper.
//This can easily be removed later in the controller if that's not needed either
$items = (!$depth && !array_key_exists('data', $items)) ?
['data' => $items] : $items;
return $items;
};
//Return the results in the form of an instance of Collection
return new static($recursion($case, $this->items));
});
/**
* Maintain non-sequential numeric keys when performing
* \Illuminate\Support\Collection::mapWithKeys() functionality
*
* Source: https://github.com/laravel/framework/issues/15409#issuecomment-247083776
*/
collect()->macro('mapWithKeys_v2', function ($callback) {
$result = [];
foreach ($this->items as $key => $value) {
$assoc = $callback($value, $key);
foreach ($assoc as $mapKey => $mapValue) {
$result[$mapKey] = $mapValue;
}
}
return new static($result);
});
Then run your Fractal results through it:
$results = collect($fractalResults)->fractal('camel_case')->get('data', []);

CakePHP 1.3 paginate with custom query

I've managed to over-ride the default methods for a custom query in my model as suggested elsewhere,
function paginate($conditions, $fields, $order, $limit, $page = 1, $recursive = null, $extra = array())
and
function paginateCount($conditions = null, $recursive = 0, $extra = array())
Unfortunately this approach over-rides all pagination for this model, and affects other pagination elsewhere. I found some code which may help where I could select whether I wanted the custom pagination used based on a variable e.g.
In my model
var $useCustom = false;
function paginateCount($conditions = null, $recursive = 0, $extra = array())
{
if(!$this->useCustom)
return parent::paginateCount($conditions, $recursive);
// code to handle custom paginate count here
}
I have found that using this method gives me an error,
Fatal error: Call to undefined method
AppModel::paginateCount() in....
What am I doing wrong? I assume that I would also need similar code in the paginate function as well? Am I also correct in thinking that I can set this variable in my controller i.e. $this->useCustom = 'true';
After a bit of delving into the code I found that the methods of paginateCount and paginate do not exist in the Model, or anywhere else for that matter, which is why I could not call them. The solution was copy the code from the main controller, which tests for the existence of the over-ride
For those that would like a similar solution use the following in paginateCount
if(!$this->useCustom)
{
$parameters = compact('conditions');
if ($recursive != $this->recursive) {
$parameters['recursive'] = $recursive;
}
$count = $this->find('count', array_merge($parameters, $extra));
return $count;
} else {
custom method...
}
and in paginate use
if(!$this->useCustom)
{
$parameters = compact('conditions', 'fields', 'order', 'limit', 'page');
if ($recursive != $this->recursive) {
$parameters['recursive'] = $recursive;
}
$results = $this->find('all', array_merge($parameters, $extra));
return $results;
} else {
custom method...
}
Hope this helps someone else.
I think you need the public keyword before your function to use the scope resolution operator in this way.
i.e. public function paginateCount(....

Categories