Laravel alternative to paginate on collection? - laravel

On my website, I have Submissions, and submissions can have comments.
Comments can have upvotes and downvotes, leading to a total "score" for the comment.
In this example, before passing the comments to the view, I sort them by score.
$comments = Comment::where('submission_id', $submission->id)->where('parent_id', NULL)->get();
$comments = $comments->sortByDesc(function($comment){
return count($comment['upvotes']) - count($comment['downvotes']);
});
This works fine. The higher the score of a comment, the higher it is sorted.
However, I want to paginate these results.
If I do ->paginate(10) instead get(), the following sortByDesc will only sort those 10 results.
So logically I would want to add the paginator after the sortByDesc like so:
$comments = $comments->sortByDesc(function($comment){
return count($comment['upvotes']) - count($comment['downvotes']);
})->paginate(10);
However this will return the error:
Method Illuminate\Database\Eloquent\Collection::paginate does not
exist.
as expected.
My question is, what is the alternative to using paginate in this situation?
EDIT:
When trying the response of #party-ring (and switching the double quotes and single quotes) I get the following error:
SQLSTATE[42000]: Syntax error or access violation: 1064 You have an
error in your SQL syntax; check the manual that corresponds to your
MariaDB server version for the right syntax to use near '["upvotes"])
- count($comment["downvotes"]) desc limit 10 offset 0' at line 1 (SQL: select * from comments where submission_id = 1 and parent_id is
null order by count($comment["upvotes"]) -
count($comment["downvotes"]) desc limit 10 offset 0)

You are trying to paginate after the get, the solution i try on my website is this and it works
$users = User::where('votes', '>', 100)->get();
$page = Input::get('page', 1); // Get the ?page=1 from the url
$perPage = 15; // Number of items per page
$offset = ($page * $perPage) - $perPage;
return new LengthAwarePaginator(
array_slice($users->toArray(), $offset, $perPage, true), // Only grab the items we need
count($users), // Total items
$perPage, // Items per page
$page, // Current page
['path' => $request->url(), 'query' => $request->query()] // We need this so we can keep all old query parameters from the url
);

You could add a macro:
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
);
});
}
Then you can use a collection to paginate your items:
collect([1,2,3,4,5,6,7,8,9,10])->paginate(5);
See Extending Collections under Introduction

Give this a try:
$comments = Comment::where('submission_id', $submission->id)
->where('parent_id', NULL)
->orderBy(DB::raw("count($comment['upvotes']) - count($comment['downvotes'])"), 'desc')
->paginate(10);`
SortBy returns a Collection, whereas you can only call paginate on an instance of QueryBuilder. OrderBy should return an instance of QueryBuilder, and you should be able to do the subtraction using a DB::raw statement.
** edit
I have just read about orderByRaw, which might be useful in this scenario:
$comments = Comment::where('submission_id', $submission->id)
->where('parent_id', NULL)
->orderByRaw('(upvotes - downvotes) desc')
->paginate(10);`
You might have to play around a bit with your subtraction above as I don't know the structure of your comments table.
A couple of links which might be useful:
laravel orderByRaw() on the query builder
https://laraveldaily.com/know-orderbyraw-eloquent/

Related

Laravel : Update Stock Value After Order Placement

I want to update stock value when order will place. The challenge is that the products have different attributes place in the attribute table.
Small, Medium, Large, Extra large
I want to update the product attribute stock respectively when order is place. For example when user place order user selected products like
product_id = 1 size Small quantity is 7
So 7 quantity must be decrement from the product attribute size Small column.
Checkout
// checkout
public function checkout(Request $request)
{
if ($request->isMethod('post')) {
$data = $request->all();
DB::beginTransaction();
// Get cart details (Items)
$cartItems = Cart::where('user_id', Auth::user()->id)->get()->toArray();
foreach ($cartItems as $key => $item) {
# code...
$cartItem = new OrdersProduct;
$cartItem->order_id = $order_id;
$cartItem->user_id = Auth::user()->id;
// Get products details
$getProductDetails = Product::select('product_code', 'product_name', 'product_color')->where('id', $item['product_id'])->first()->toArray();
$cartItem->product_id = $item['product_id'];
$cartItem->product_code = $getProductDetails['product_code'];
$cartItem->product_name = $getProductDetails['product_name'];
$cartItem->product_color = $getProductDetails['product_color'];
$cartItem->product_size = $item['size'];
$getDiscountedAttrPrice = Product::getDiscountedAttrPrice($item['product_id'], $item['size']);
$cartItem->product_price = $getDiscountedAttrPrice['final_price'];
$cartItem->product_qty = $item['quantity'];
$cartItem->save();
// Want to Update the Product Attribute Table Stock
$item = new ProductsAttribute;
$item->where('product_id', '=', $item['product_id'], 'size', '=', $item['size'])->decrement('stock', $request->quantity);
}
// Insert Order Id in Session variable for Thanks page
Session::put('order_id', $order_id);
DB::commit();
}
}
When i run this code it shows me an error
InvalidArgumentException
Non-numeric value passed to decrement method.
When i enter value directly like decrement('stock', 7) it shows and error
Illuminate\Database\QueryException
SQLSTATE[42000]: Syntax error or access violation: 1064 You have an error in your SQL syntax; check the manual that corresponds to your MariaDB server version for the right syntax to use near '`product_id` is null' at line 1 (SQL: update `products_attributes` set `stock` = `stock` - 7, `products_attributes`.`updated_at` = 2021-05-01 17:18:30 where size `product_id` is null)
i search alot but yet not find any solution. Please any one help
Just change the update query and make it simple and clean. The rest of the code is fine.
// Want to Update the Product Attribute Table Stock
$product_attribute = ProductsAttribute::where(['product_id' => $item['product_id'], 'size' => $item['size']])->first();
if($product_attribute){
$stock = $product_attribute->stock - (int) $request->quantity;
$product_attribute->update(['stock' => $stock]);
}
you have to warp it in an array or use two of wheres better and understandable
$item->where([
['product_id', '=', $item['product_id']],
['size', '=', $item['size']],
])->decrement('stock', (int) $request->quantity);
Or like this:
$item->where('product_id', $item['product_id'])
->where('size', $item['size'])
->decrement('stock', (int) $request->quantity);

Laravel eloquent query loads hundreds of models

I'm querying a relationship with pagination, yet in my debugbar I can see that all models are loaded in memory and if I'm correct that should not be happening.
I have a Post model with a hasMany relationship to Comments. I have a few lines of code as below. They are written in this order because there are parameters in between that I need to apply. I have shown filterScore here but there are multiple that work the same way.
$post = Post::find(1);
$comments = $post->comments();
$comments = $comment->filterScore($comments)
$comments = $comments->orderBy('created_at', 'DESC');
return $comments->paginate(25);
private function filterScore($q)
{
if($this->score > 0)
return $q->where('score', $this->score);
return $q;
}
The raw query if $this->score = 0:
select * from `comments` where `comments`.`post_id` = 1 and `comments`.`post_id` is not null order by `created_at` desc limit 25 offset 0
UPDATE
I've tried writing it like this, based on this post, but then I still get the same result: all models are loaded into memory.
$post = Post::find(1);
$comments = Comment::query();
$comments = Comment::where('post_id', $post->id);
$comments = $comment->filterScore($comments);
return $comments->paginate(25);
In the Laravel debugbar you can see that all models are loaded into memory, instead of just 25:
One solution is, as #nikistag wrote in comment: $post->comments()->orderBy('created at' , 'DESC')->paginate(25); (you have to have all in one chained expression).
But if you use optional parameters, it can be difficult to achieve someting like that.
In this case, you can just change it to 2 seperate codes, where you do not use laravel relationship:
$post = Post::find(1);
$comments = Comment::where('post_id', $post->id);
if(!empty($from)) //if set optional parameter, add condition
$comments->where('created_at', '>=', $from);
$comments->orderBy('created_at', 'desc')->paginate(25);

Laravel simplePaginate() for Grouped Data

I have the following query.
$projects = Project::orderBy('created_at', 'desc');
$data['sorted'] = $projects->groupBy(function ($project) {
return Carbon::parse($project->created_at)->format('Y-m-d');
})->simplePaginate(5);
When I try to paginate with the simplePaginate() method I get this error.
stripos() expects parameter 1 to be string, object given
How can I paginate grouped data in this case?
The created_at attribute is already casted as a Carbon Object (by default in laravel models). that's why you are getting that error. Try this:
$projects = Project::orderBy('created_at', 'desc')->get();
$data['sorted'] = $projects->groupBy(function ($project) {
return $project->created_at->format('Y-m-d');
})->simplePaginate(5);
this answer is just for the error you're getting. now if you want help with the QueryBuilder, can you provide an example of the results you're expecting to have and an example of the database structure ?
The pagination methods should be called on queries instead of collection.
You could try:
$projects = Project::orderBy('created_at', 'desc');
$data['sorted'] = $projects->groupBy('created_at');
The problem was solved. I was create custom paginator via this example:
https://stackoverflow.com/a/30014621/6405083
$page = $request->has('page') ? $request->input('page') : 1; // Use ?page=x if given, otherwise start at 1
$numPerPage = 15; // Number of results per page
$count = Project::count(); // Get the total number of entries you'll be paging through
// Get the actual items
$projects = Project::orderBy('created_at', 'desc')
->take($numPerPage)->offset(($page-1)*$numPerPage)->get()->groupBy(function($project) {
return $project->created_at->format('Y-m-d');
});
$data['sorted'] = new Paginator($projects, $count, $numPerPage, $page, ['path' => $request->url(), 'query' => $request->query()]);
simplePaginate Method is exist in the path below:
Illuminate\Database\Eloquent\Builder.php::simplePaginate()

How to Use Union And Paginate in laravel

I Want To Union two Model And Paginate Result But Get Error, In Bellow See My Call Code And Result:
$this->archive = $this->archive
->select (['id','property_id','plan_id','pay_id','price','period','start_at','expire_at'])
->whereHas('property',function ($query) use ($user){
$query->select(['user_id','type_id','title'])->where('user_id',$user);
})->with(['property'=>function($query){
$query->select('title','id');
},'property.owner'=>function($query){
$query->select('name','family','id');
},'plan'=>function($query){
$query->select('title','price','id','status');
},'transaction'=>function($query){
$query->select('port','price','id','status','payment_date');
}]);
$this->model = $this->model
->select (['id', 'property_id','plan_id','pay_id','price','period','start_at','expire_at'])
->whereHas('property',function ($query) use ($user){
$query->select(['user_id','type_id','title'])->where('user_id',$user);
})->with(['property'=>function($query){
$query->select('title','id');
},'property.owner'=>function($query){
$query->select('name','family','id');
},'plan'=>function($query){
$query->select('title','price','id','status');
},'transaction'=>function($query){
$query->select('port','price','id','status','payment_date');
}])
->union($this->archive)->orderBy('expire_at','DESC')->paginate ($paginate);
And Result Of Them Is:
SQLSTATE[21000]: Cardinality violation: 1222 The used SELECT statements have a different number of columns
Please Help Me
you can union sql results like this,
$result = $query1->merge($query2);
$resultSorted = $result->sortByDesc('expire_at');
$count = $query1->count() + $query2->count();
$page = $request['page'];
$perPage = 20;
$resultSorted = new LengthAwarePaginator(
$resultSorted->forPage($page, $perPage), $count, $perPage, $page
);
Pagination with union is not supported in Laravel since there's no elegant way to do it without major drawbacks.

Laravel cache::remember is returing object as an array

Laravel Cache::remember is returning a LengthAwarePaginator object as an array.
function getNotifications( $userID ) {
$values = Cache::remember('cache-key', 10, function() {
$query = DB::table( 'user_notifications' )
->leftJoin( 'notifications', 'user_notifications.notification_id', '=', 'notifications.id' )
->where( 'user_notifications.user_id', $userID )
->select( 'notifications.*' )
->orderBy('created_at', 'DESC')
->paginate(5);
return $query;
});
return $values;
}
If I dd($query) before returning from Cache closure, it's returning the following object, that accepts $value->links() to display pagination.
But whenever the Cache is storing $query into $values it's returning values as an array:
I tried commenting out the unserialize-block:
/*foreach( $values as $key => $value ) :
$values[$key]->meta = self::maybeUnserialize($value->meta);
endforeach;*/
and confirmed that, that's not the cause.
I also tried, but failed:
$values = collect($values);
With multiple check and cross-check I am confirming that, the issue is the Cache::remember.
How can I force the Cache::remember return things as it is? So that I can let $object->links() work for me.
The actual code can be found here.
The issue is, Cache is made to store data, not the instance. So there are two ways to do this:
When caching, cache information per page, or
Get all the data while fetching, but make your own pagination
Solution 1:
We went for the second. But in case you need to go for the first solution, here's the code, I got from Laracasts, provided by chrisml:
$statuses = Cache::remember("statuses_{$id}_page_{$page}", 3, function() use ($event, $sort) {
return $event->statuses()
->with('comments')
->latest()
->paginate(10);
});
On the above code, the cache key is changing on every page, so the cache is storing per page.
Solution 2:
But for my case, we thought we should go for the second, and that would be wise for us, in our case. So, we've to make our own pagination. My luck that psampaz did the base for us on their blog:
Custom data pagination with Laravel 5 — by psampaz
So, instead of using ->paginate() we're fetching all the data first, and caching them as previous.
$values = Cache::remember('cache-key', 10, function() {
$query = DB::table( 'user_notifications' )
->leftJoin( 'notifications', 'user_notifications.notification_id', '=', 'notifications.id' )
->where( 'user_notifications.user_id', $userID )
->select( 'notifications.*' )
->orderBy('created_at', 'DESC')
->get(); // <----------- here
return $query;
});
But before returning the $values, we're making our own pagination. We made some fixes to the psampaz's code:
use Illuminate\Support\Collection;
use Illuminate\Pagination\LengthAwarePaginator;
function getNotifications( $userID ) {
// Collapsed the cached values here :)
$values = Cache::remember('cache-key', 10, function() {...});
// Get current page form url e.g. &page=6.
$currentPage = LengthAwarePaginator::resolveCurrentPage();
// Get current path.
$currentPath = LengthAwarePaginator::resolveCurrentPath();
// Create a new Laravel collection from the array data.
$collection = new Collection($values);
// Define how many items we want to be visible on each page.
$perPage = 5;
// Slice the collection to get the items to display on the current page.
$results = $collection->slice(($currentPage - 1) * $perPage, $perPage)->all();
// Create our paginator and pass it to the view.
$values = new LengthAwarePaginator($results, count($collection), $perPage, $currentPage, ['path' => $currentPath]);
return $values;
}
And finally, we can easily use the $object->links() for pagination, and it's awesome! :)

Resources