Magento product collection pagination with custom sort - php

I'm overriding Mage_Catalog_Block_Product_List 's _getProductCollection by adding:
foreach ($this->_productCollection as $product) {
$product->setDistance(Mage::helper('myhelper')->getDistance($product));
}
Now I want the collection to be sorted by distance, I tried the following:
$this->_productCollection = Mage::helper('myhelper')->sortProductByDist($this->_productCollection);
The helper for sorting is like following (stolen from SO):
public function sortProductByDist($products) {
$sortedCollection = Mage::getSingleton('catalog/layer')
->getProductCollection()->addFieldToFilter('entity_id', 0);
$sortedCollection = $sortedCollection->clear();
$collectionItems = $products->getItems();
usort($collectionItems, array($this,'_sortItems'));
foreach ($collectionItems as $item) {
$sortedCollection->addItem($item);
}
return $sortedCollection;
}
protected function _sortItems($a, $b) {
$order = 'asc';
$al = strtolower($a->getDistance());
$bl = strtolower($b->getDistance());
if ($al == $bl) {
return 0;
}
if ($order == 'asc') {
return ($al < $bl) ? -1 : 1;
} else {
return ($al > $bl) ? -1 : 1;
}
}
The problem is the product collection is no longer paginated when this additional sort is applied.
Anyone knows how to fix this?

You are not doing it the right way, and there are no easy solutions. You need to use the database to do the sorting.
The _productCollection is not an array, it's an object that has references, the query at this point can still be updated, the pagination will be handled by the query to the database.
if you do a
Mage::log((string) $this->_productCollection->getSelect());
you will see the query in the logs
What you do is to load the products of the current page, add the distance on all products of the page, and create a new collection where you force your items in. So that collection's data is not coming from the database and only contains the elements of the current page.
Sorting using php is a bad idea, because if you have a lot of products it means you need to load them all from the database. That will be slow.
The solution
Calculate distance in the database directly by modifying the query.
You can edit the select query and do the distance calculation in the database
$this->_productCollection
->getSelect()
->columns("main.distance as distance")
Now you can add a sort on the product collection
$this->_productCollection->setOrder('distance');
The complicated part will be to write the equivalent of your getDistance method in mysql. In my example I assumed distance was in the database already.
Don't hesitate to print the query at various steps to understand what is going on.

Related

pagination logic without offset usage by max id as pagination filter condition for php codeigniter framework

i see there are few methods we can apply pagination, an one of it is utilizing CI pagination library that generates links for us to navigate.
being main logic centered to
fetching limited data relevant to display
and iterate as move across pagination links.. here is my model logic without offset usage(for performance to use primary key id)
using limit, maxid of last run ,usertype; so next run should on rows greaterthan supplied id.
in controller say i am collecting results to data["results"] as
$data["results"] = $this->ressults_model->fetch_data($config["per_page"],$maxidvalue,$usertype);
in model.
public function fetch_data($limit,$maxid,$usertype) {
$date = date('Y-m-d');
$this->db->limit($limit);
$this->db->order_by('post_date', 'DESC');
$this->db->select('id,title');
if ($usertype == 1)
{$query = $this->db->get_where('titles',array('id >' => $id,'expiry_date >' => $date));}
else
{$query = $this->db->get_where('titles',array('id >' => $id,'expiry_date >' => $date,'visibility'=>'All'));}
if ($query->num_rows() > 0)
{ return $query->result(); }
return array();
}
challenge how to collect and pass the maxid recursively for pagination to work.
appreciate some pointers.
Thanks
Solved!
i have used session variable maxid that stores the max value and feeds the fetch_data method as parameter after every result display.
foreach ($data["results"] as $row) {
if ( $this->session->get_userdata('sessiondata')['maxid'] > $row->id)
$this->session->unset_userdata('maxid');
$this->session->set_userdata('maxid',$row->id);
echo "new value".$this->session->get_userdata('sessiondata')['maxid'];
}
please post if you have any better solution to solve this.
Thanks!

Adding items into array Laravel using for loop

I am trying to get all categories that have products from the database and push them into another array.
I have four 3 categories and two of them have products.
Here is my code:
$categories = Category::all();
$count = count($categories);
$categoriesWithProducts = array();
for($i = 0; $i < $count; $i++) {
if($categories[$i]->products->count() > 0) {
array_push($categoriesWithProducts, $categories[$i]);
}
return response()->json($categoriesWithProducts);
}
I get an array with just one item instead of two.
Where am i going wrong?
Although error is obvious (mentioned in comment) you could rewrite the whole thing:
$categories = Category::withCount('products')->get(); // you load count to not make n+1 queries
$categoriesWithProducts = $categories->filter(function($category) {
return $category->products_count > 0
})->values();
return response()->json(categoriesWithProducts);
Of course you could make it even simpler:
return response()->json(Category::withCount('products')->get()
->filter(function($category) {
return $category->products_count > 0
})->values()
);
But in fact the best way would be using Eloquent relationships so you could use:
return response()->json(Category::has('products')->get());

Getting a specific eloquent object out of a collection in Laravel

I have a function that looks for possible boxes that can carry the article.
public static function get_possible_boxes($article,$quantity)
{
$i = 0;
$possible_boxes = array();
$total_weight = $articles->grams * $quantity;
$boxes = Boxes::all();
foreach($boxes as $box)
{
if($total_weight+ $box->grams < $box->max_weight)
{
$possible_boxes[$i] = $box;
$i++;
}
}
return collect($possible_boxes);
}
This gives me a collection with boxes that can carry my items.
Now I should check if the ID of the box selected by the customer exists. If it does not exist, it will pick the first valid one.
This is where I am stuck. I have tried to use puck:
public function someotherfunction(){
...
$boxes = get_possible_boxes($something,$number);
$valid_box = $boxes->where("id", $request->selected_box)->pluck("id");
if(!$valid_box){
$valid_box = $boxes[0]
}
...
This works if the selected box cannot be used. The function pluck only gives me the id, clearly it is not the function I am looking for and I have already read the Laravel documentation.
So the question is, how do I get the correct eloquent model ?
You're looking for the first() method.
$valid_box = $boxes->where("id", $request->selected_box)->first();
Alternatively, if you rework your get_possible_boxes() method to return a Illuminate\Database\Eloquent\Collection instead of a plain Illuminate\Support\Collection, you could use the find() method, like so:
Function:
public static function get_possible_boxes($article,$quantity)
{
$total_weight = $article->grams * $quantity;
$boxes = Boxes::all()->filter(function ($box) use ($total_weight) {
return $total_weight + $box->grams < $box->max_weight;
});
return $boxes;
}
Find:
$boxes = get_possible_boxes($something, $number);
$valid_box = $boxes->find($request->selected_box) ?? $boxes->first();
And you could probably squeeze out a little more performance by adding the weight condition as part of the SQL query instead of filtering the collection after you've returned all the boxes, but I left that up to you.
What you want is probably filter.
$valid_box = $boxes->filter(function($box) use ($request){
return $box->id == $request->selected_box;
});
if($valid_box)...
I should note that if you don't want $valid_box to be a collection, you can use first instead of filter in the exact same way to only get the object back.
It could be done in many ways but I would rather use the following approach:
$boxes = get_possible_boxes($something,$number)->keyBy('id');
$valid_box = $boxes->get($request->selected_box) ?: $boxes->first();

How to assign a dynamic property in a model via a SQL query in Laravel 5

I'm using Laravel 5.3 to build an API and I have an model for products. Whenever I retrieve a product, I want to retrieve the product's rating and it's recommended rate. I also have a model for reviews and products have many reviews.
$product = Product::where('slug', $slug)->with('reviews')->first()->toArray();
Rating is computed by looping through $product->reviews in the controller, adding up the score of each review, then dividing it by the total number of reviews.
if (count($product['reviews']) > 0) {
$i = 0;
$totalScore = 0;
foreach ($product['reviews'] as $review) {
$totalScore = $totalScore + $review['Rating'];
$i++;
}
$product['averageReviewRating'] = $totalScore / $i;
} else {
$product['averageReviewRating'] = null;
}
Recommended rate is computed with a SQL query.
$product['recommendedRate'] = round(DB::select("
select ( count ( if (Recommend = 1,1,NULL) ) / count(*)) * 100 as rate
from Review where PrintProduct_idPrintProduct = " . $product['idPrintProduct']
)[0]->rate);
This leaves me with $product['averageReviewRating'] and $product['recommendedRate'] with the data I want but seems very sloppy. I would like to just be able to do something similar to this below and have those two values assigned to each object of a collection, than access them via $product->averageReviewRating and $product->recommendedRate or even not include them in with and have those values eagerly assigned.
$product = Product::where('slug', $slug)->with(['Reviews', 'RecommendedRate', 'AverageReviewRating'])->first();
Anyone know a way to do this with ORM? I've looked high and low and have not found anything.
You can do this way
protected $appends = [
'reviews',
'recommendedRate',
'averageReviewRating'
];
public function getReviewsAttribute() {
return $this->reviews()->get();
}
public function getRecommendedRateAttribute() {
if (count($this->reviews) > 0) {
$i = 0;
$totalScore = 0;
foreach ($this->reviews as $review) {
$totalScore = $totalScore + $review->Rating;
$i++;
}
return $totalScore / $i;
} else {
return null;
}
}
public function getAverageReviewRatingAttribute() {
return round(DB::select("
select ( count ( if (Recommend = 1,1,NULL) ) / count(*)) * 100 as rate
from Review where PrintProduct_idPrintProduct = " . $this->idPrintProduct
)[0]->rate);
}
then simply call Product::where('slug', $slug)->first()->toArray();
P.S. This is just the way you can do, I might miss part of logic or names..
The way to get the sum in Laravel Eloquent is using the Aggregate sum and for average avg
https://laravel.com/docs/5.4/queries#aggregates
If you want to add a custom property to your model for that, you can use
class Product {
function __construct() {
$this->{'sum'} = DB::table('product')->sum();
$this->{'avg'} = DB::table('product')->avg();
}
}
edit: to set the attributes, you can use the built in function https://github.com/illuminate/database/blob/v4.2.17/Eloquent/Model.php#L2551

Laravel hasMany relation count

There are many Categories(Category.php) on my index site, which has many Boards(Board.php).
A Board can have many Topics(Topic.php), and a Topic can have many Replies (Reply.php).
I want to fetch the number of replies of the current board, possibly with eager loading.
I have a way, but it executes too many queries. I create a post_count attribute, then I iterate over every topic and gather number of replies. Is there a way to select all replies from the current board?
public function getPostCountAttribute()
{
$count = 0;
foreach ($this->topics()->withCount('replies')->get() as $topic) {
$count += $topic->replies_count;
}
return $count;
}
Found a nice way. Even though, I'll leave this question open, if anyone finds a better way to do it.
I declared a hasManyThrough relationship, and the called the count():
Board.php:
public function replies()
{
return $this->hasManyThrough(Reply::class, Topic::class);
}
And then called:
$board->replies->count()
30 queries executed (45 before).
Still, I would like to find an eager way to load the data, getting the query number down to 2 or 3 queries.
In addition to your reply you can do something like this.
public function repliesCount() {
return $this->replies()->selectRaw('board_id, count(*) as aggregate')
->groupBy('board_id');
}
and to use this use as below
$board->repliesCount()
Please note that you have to change query according to you.
Try it
Change
$count = 0;
foreach ($this->topics()->withCount('replies')->get() as $topic) {
$count += $topic->replies_count;
}
To
$topic_ids = $this -> topics -> pluck('id');
$reply_count = Reply::whereIn('id',$topic_ids) -> count();

Categories