Getting a specific eloquent object out of a collection in Laravel - php

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();

Related

laravel - orderby doesn't show correct results

I have a fairly simple query which I load and add an element to the object and then sort it based on that custom element. I'd like to take 20 records or paginate it but when I do so, alot of data vanishes.
This is my code. The piece below gets all the records.
$fighters = Fighter::all();
The below code gets the points which is in a function and adds it to the fighter, fighterPoints does not initially exist in the collection, it is created and populated below.
foreach ($fighters as $fighter) {
$fighter->fighterPoints = $fighter->getFighterPoints($fighter->id);
}
Then i'd like to sort everything by those fighterPoints with the below function.
$sorted = $fighters ->sortByDesc(function ($item, $key) {
return $item->fighterPoints ;
});
When i do this i get all the records which are around 9000, it then sorts on the fighterPoints correctly:
The first record being something like [FighterName, 21309]
When i do $fighters = Fighter::paginate(20); it simply starts with [FighterName384, 200] which should be [FighterName, 21309] and just 20 results. The same thing happens with ::take(20) method.
What am I doing wrong here? I am laravel 8.
You want to paginate your $sorted variable right?!
To do that you have to
use Illuminate\Pagination\Paginator; // add
use Illuminate\Support\Collection; // add
use Illuminate\Pagination\LengthAwarePaginator; //add
...(your code)
// add
public function paginate($items, $perPage = 5, $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);
}
then, use that $sorted this way:
$fighters = $this->paginate($sorted);
reference:https://www.itsolutionstuff.com/post/laravel-6-paginate-with-collection-or-arrayexample.html
—————- EDIT ————
Sorry I misunderstood your question!
If you want to order eloquent, here is how you do it
$fighters = Fighter::orderBy('fighterPoints', 'desc')->paginate(20);
I hope that’s what you are looking for!

Magento product collection pagination with custom sort

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.

How do I use whereYear inside filter Laravel

I have a collection that I am trying to filter on created_at year. Laravel 5.3. I am trying to use whereYear inside my filter but it's sqwaking that the method is undefined. How do I define it? Or is there a better way?
$datas = Campaign::all();
if($request->year) {
$value = $request->year;
$datas = $datas->filter(function($data) use ($value) {
return $data->created_at->whereYear($value);
});
}
I would do this more compact:
So in your Controller you can do:
$datas = ($request->year)
? Campaign::inYear($request->year)->get()
: Campaign::all();
The advantage is, that you get only the record that you need from the DB, and don't have to do any kind of filter on a Collection. This will also increaser your performance a little bit.
In your Campaign Model you add on inYear scope that is than reusable:
use Carbon\Carbon;
class Campaign extends Model
{
public function scopeInYear($query, $year)
{
return $query->whereBetween('created_at', [
Carbon::create($year)->startOfYear(),
Carbon::create($year)->endOfYear(),
]);
}
}
In laravel created_at is an instance of Carbon so :
$datas = Campaign::all();
if($request->year) {
$value = $request->year;
$datas = $datas->filter(function($data) use ($value) {
return $data->created_at->year == $value;
});
}
whereYear(...) is shorthand for where('year', ...) in Laravel, but you want to be searching against created_at, so that's not going to work. Something like this should do the trick, and reduce the number of records you have to fetch from the DB as well:
// better to let the database handle the search
// than fetching all records and filtering the collection
$query = Campaign::query();
if($request->year) {
// be sure to validate $request->year somewhere, for obvious reasons...
$query->whereBetween('created_at', $request->year . '-01-01 00:00:00', $request->year . '-12-31 23:59:59')
}
return $query->get();

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 5.2 - filtering on a custom attribute and then paginating

So I know how to paginate using paginate() and I know how to filter based on an Accessor (a where() on the collection). However, paginate takes in a query builder and where() on a collection returns a collection.
So if I want to get a bunch of items / filter by a custom attribute and then paginate the result set....how do i do that??
Accessor:
public function getRequiredToReportAttribute()
{
// return boolean based off of complicated business logic
}
index method:
public function index()
{
//what im doing (redacted)
$employers = (new App\Employers')->paginate($this->perPage);
// what I would like to be doing
$employers = (new App\Employers)->where('required_to_report', '=', true)->paginate($this->perPage);
return $this->sendResponse($employers);
}
In the case that you want to work with accesors, you could by iterating the collection after you get your query, something like this:
$result = Model::get()->filter(function($item) {
return $item->require_to_report === true;
});
Here you have all records of your model and then you could create a manual paginator:
$paginator = new Illuminate\Pagination\Paginator($result, 10);
you have with this approach a weakness when you have too many records, the performance could be affected.
Based off of Jose Rojas answer and this post I built a LengthAwarePaginator for a collection filtering on an attribute accessor. Here's an example of how to do it:
$collection = Model::all();
//Filter your collection
$filteredItems = $collection->filter(function($col) {
return $col->require_to_report === true;
});
// Setup necessary information for LengthAwarePaginator
$currentPage = LengthAwarePaginator::resolveCurrentPage();
$pageLimit = 20;
// slice the current page items
$currentItems = $filteredItems->slice(pageLimit * ($currentPage - 1), pageLimit)->values();
// you may not need the $path here but might be helpful..
$path = "/api/v1/employers";
// Build the new paginator
$paginator = new LengthAwarePaginator($currentItems, count($filteredItems), $pageLimit, $currentPage, ['path' => $path]);
return $paginator;

Categories