Merging multiple objects which uses same id - laravel

I'm trying to merge multiple objects (like Receipts, Reports, etc) with Collection->merge().
This is the code I used:
$receipts = Receipt::all();
$reports = Report::all();
$collection = $receipts->merge($reports);
This is the result:
The above screenshot shows two elements, but the third element is missing because it has the same id (id: "1") as the first one. What I'm trying to achieve is to display all three of them as a collection.
EDIT:
I need the result to be objects (collection) because I also use the code on my view, where I check the class to determine what to display. Also, I use this function to sort the objects in the collection.
$collection->sort(function($a, $b)
{
$a = $a->created_at;
$b = $b->created_at;
if ($a === $b) {
return 0;
}
return ($a > $b) ? 1 : -1;
});

I know that this is an old question, but I will still provide the answer just in case someone comes here from the search like I did.
If you try to merge two different eloquent collections into one and some objects happen to have the same id, one will overwrite the other. I dunno why it does that and if that's a bug or a feature - more research needed. To fix this just use push() method instead or rethink your approach to the problem to avoid that.
Example of a problem:
$cars = Car::all();
$bikes = Bike::all();
$vehicles = $cars->merge($bikes);
// if there is a car and a bike with the same id, one will overwrite the other
A possible solution:
$collection = collect();
$cars = Car::all();
$bikes = Bike::all();
foreach ($cars as $car)
$collection->push($car);
foreach ($bikes as $bike)
$collection->push($bike);
Source: https://medium.com/#tadaspaplauskas/quick-tip-laravel-eloquent-collections-merge-gotcha-moment-e2a56fc95889

I know i'm bumping a 4 years old thread but i came across this and none of the answers were what i was looking for; so, like #Tadas, i'll leave my answer for people who will come across this. After Looking at the laravel 5.5 documentation thoroughly i found that concat was the go-to method.
So, in the OP's case the correct solution would be:
$receipts = Receipt::all();
$reports = Report::all();
$collection = $receipts->concat($reports);
This way every element in the Report collection will be appended to every element in the Receipts collection, event if some fields are identical.
Eventually you could shuffle it to get a more visual appealing result for e.g. a view:
$collection->shuffle();

Another way to go about it is to convert one of your collections to a base collection with toBase() method. You can find it in Illuminate\Support\Collection
Method definition:
/**
* Get a base Support collection instance from this collection.
*
* #return \Illuminate\Support\Collection
*/
public function toBase()
{
return new self($this);
}
Usage:
$receipts = Receipt::all();
$reports = Report::all();
$collection = $receipts->toBase()->merge($reports);

You could put all collections in an array and use this. Depends on what you want to do with the collection.
$list = array();
$list = array_merge($list, Receipt::all()->toArray());
$list = array_merge($list, Report::all()->toArray());

Related

How to sort Eloquent collection after fetching results?

In my controller I have this query
$query = Market::whereHas('cities', function($query) use ($city) {
$query->where('id', $city);
})->get();
Then I want to make a few operations with this collection and remove my subquerys from the main object
$return['highlighted'] = $markets->where('highlighted', true);
$markets = $markets->diff($return['highlighted']);
The problem is when I try to sort it by created_at
$return['latest'] = $markets->sortByDesc('created_at')->take(4);
$markets = $markets->diff($return['latest']);
It just won't work, keeps returning the first 4 objects order by id, I've tried parsing created_at inside a callback function with Carbon::parse() and strtotime with no results.
I'm avoiding at all cost to make 2 different database querys since the original $markets has all the data that I need.
Any suggestion?
Thanks
I think your problem is you try to sort and take in one-go and also missing a small thing when you try to access the values of the collection.
Try below approach:
$return['latest'] = $markets->sortByDesc('created_at');
$markets = $markets->diff($return['latest']->values()->take(4));
Or you may need to do it like:
$return['latest'] = $markets->sortByDesc('created_at');
$return['latest'] = $return['latest']->values()->take(4);
$markets = $markets->diff($return['latest']->all());

Combine 2 collections (keep the similar ones)

I've several collections, I want to keep only the elements that are present in each collection.
I went through the available methods, but I didn't find anything that would match.
$candidatesByConsultant = Consultant::find(request('consultant_id'))->candidates;
$candidatesByCreation = Candidate::whereBetween('created_at',[Carbon::parse(request('meeting_since')), Carbon::parse(request('meeting_to'))])->get();
Do you have any idea? :)
In order to have values that only present in both collection you must use intersect method:
$result = $candidatesByConsultant->intersect($candidatesByCreation);
The intersect method intersects the values of both collections. You can read it in Laravel's official documentation.
And in order to get have results that are not present in both collection you must use diff method:
$result = $candidatesByConsultant->diff($candidatesByCreation);
The diff method finds differences between collections. You can read it in Laravel's official documentation.
The intersect method may be suitable : https://laravel.com/docs/5.8/collections#method-intersect
Example taken from the documentation:
$collection = collect(['Desk', 'Sofa', 'Chair']);
$intersect = $collection->intersect(['Desk', 'Chair', 'Bookcase']);
$intersect->all();
// [0 => 'Desk', 2 => 'Chair']
However, especially if you are trying to intersect multiple collections of Eloquent models, it may not work since the equality between two models is defined by the Model::is() method. Check
https://laravel.com/docs/5.8/eloquent#comparing-models for more information about comparing two Eloquent models.
To handle this, I would do the following, assuming the primary key of your models is id:
$candidatesByConsultant = Consultant::find(request('consultant_id'))->candidates;
$candidatesByCreation = Candidate::whereBetween('created_at',[Carbon::parse(request('meeting_since')), Carbon::parse(request('meeting_to'))])->get();
$candidates = $candidatesByConsultant->merge($candidatesByCreation)->unique("id");
You may check the merge() and unique() documentations.
The built-in for this is $collection->intersect($other), but you can also achieve the desired result with a simple custom filter:
$left = collect([Model::find(1), Model::find(2), Model::find(3)]);
$right = collect([Model::find(1), Model::find(3), Model::find(5)]);
$result = $left->filter(function ($value, $key) use ($right) {
return $right->contains(function ($v, $k) use ($value) {
return $v->id === $value->id;
});
});
This will perform model comparison by id. It is not very performant though. Another approach would be to retrieve two arrays of ids, intersect them and filter the merged sets based on this list:
$left = collect([Model::find(1), Model::find(2), Model::find(3)]);
$right = collect([Model::find(1), Model::find(3), Model::find(5)]);
$merged = $left->merge($right);
$ids = array_intersect($left->pluck('id')->toArray(), $right->pluck('id')->toArray());
$result = $merged->filter(function ($value, $key) use ($ids) {
return in_array($value->id, $ids);
});

dynamic product using laravel

I'm new in web developing. I'm trying to add dynamic product to my eCommerce website. This is my database structure:
database design
options table for attributes like: size, color.
choices table for different options like: red, blue, large, small.
My question is how I can reach Instance by having choices (instances and choices have many to many relationship). For example when selecting blue and small by user I want to get the price through ajax, How I can reach Instance to return price with these choices?In other words, consider Instance and Choice models have many to many relationship. If I have one choice, I can easily reach its instances through many to many relationship. But how can I reach instances (in my case only one instance) when I have two or more choices?
Now I'm using following code, but I think there should be an easier way to do this:
$choices = Choice::findOrFail($request->choices);
$collection = collect();
foreach($choices as $choice){
$ids = $choice->instances->where('product_id',$request->product_id)->pluck('id');
$collection = $collection->merge($ids);
}
$array = $collection->toArray();
$values = array_count_values($array);
arsort($values);
$instance_id = array_slice(array_keys($values), 0, 1, true);
$instance = Instance::where('id',$instance_id[0])->first();
return $instance->price_sell;
You can use pluck() and collapse():
$choices = Choice::with('instances')->findOrFail($request->choices);
$instances = $choices->pluck('instances')->collapse();
$ids = $instances->where('product_id', $request->product_id)->pluck('id');
$values = array_count_values($ids->all());
arsort($values);
$instance = $instances->find(array_keys($values)[0]);
return $instance->price_sell;
with('instances') eager loads the instances to reduce the number of queries.

Laravel query optimization

I have a query in laravel:
...
$query = $model::group_by($model->table().'.'.$model::$key);
$selects = array(DB::raw($model->table().'.'.$model::$key));
...
$rows = $query->distinct()->get($selects);
this works fine and gives me the fields keys' that I need but the problem is that I need to get all the columns and not just the Key.
using this:
$selects = array(DB::raw($model->table().'.'.$model::$key), DB::raw($model->table().'.*'));
is not an option, cuz it's not working with PostgreSQL, so i used $rows to get the rest of columns:
for ($i = 0; $i<count($rows); $i++)
{
$rows[$i] = $model::find($rows[$i]->key);
}
but as you see this is it's so inefficient, so what can i do to make it faster and more efficient?
you can find the whole code here: https://gist.github.com/neo13/5390091
ps. I whould use join but I don't know how?
Just don't pass anything in to get() and it will return all the columns. Also the key is presumably unique in the table so I don't exactly understand why you need to do the group by.
$models = $model::group_by( $model->table() . '.'. $model::$key )->get();

Codeigniter Pagination: Run the Query Twice?

I'm using codeigniter and the pagination class. This is such a basic question, but I need to make sure I'm not missing something. In order to get the config items necessary to paginate results getting them from a MySQL database it's basically necessary to run the query twice is that right?
In other words, you have to run the query to determine the total number of records before you can paginate. So I'm doing it like:
Do this query to get number of results
$this->db->where('something', $something);
$query = $this->db->get('the_table_name');
$num_rows = $query->num_rows();
Then I'll have to do it again to get the results with the limit and offset. Something like:
$this->db->where('something', $something);
$this->db->limit($limit, $offset);
$query = $this->db->get('the_table_name');
if($query->num_rows()){
foreach($query->result_array() as $row){
## get the results here
}
}
I just wonder if I'm actually doing this right in that the query always needs to be run twice? The queries I'm using are much more complex than what is shown above.
Unfortunately, in order to paginate you must know how many elements you are breaking up into pages.
You could always cache the result for the total number of elements if it is too computationally expensive.
Yeah, you have to run two queries, but $this->db->count_all('table_name'); is one & line much cleaner.
Pagination requires reading a record set twice:
Once to read the whole set so that it can count the total number records
Then to read a window of records to display
Here's an example I used for a project. The 'banner' table has a list of banners, which I want to show on a paginated screen:
Using a public class property to store the total records (public $total_records)
Using a private function to build the query (that is common for both activities). The parameter ($isCount) we pass to this function reduces the amount of data the query generate, because for the row count we only need one field but when we read the data window we need all required fields.
The get_list() function first calls the database to find the total and stores it in $total_records and then reads a data window to return to the caller.
Remember we cannot access $total_records without first calling the get_list() method !
class Banner_model extends CI_Model {
public $total_records; //holds total records for get_list()
public function get_list($count = 10, $start = 0) {
$this->build_query();
$query = $this->db->get();
$result = $query->result();
$this->total_records = count($result); //store the count
$this->build_query();
$this->db->limit($count, $start);
$query = $this->db->get();
$result = $query->result();
return $result;
}
private function build_query($isCount = FALSE) {
$this->db->select('*, b.id as banner_id, b.status as banner_status');
if ($isCount) {
$this->db->select('b.id');
}
$this->db->from('banner b');
$this->db->join('company c', 'c.id = b.company_id');
$this->db->order_by("b.id", "desc"); //latest ones first
}
And now from the controller we call:
$data['banner_list'] = $this->banner_model->get_list();
$config['total_rows'] = $this->banner_model->total_records;
Things get complicated when you start using JOINs, like in my example where you want to show banners from a particular company! You may read my blog post on this issue further:
http://www.azmeer.info/pagination-hitting-the-database-twise/

Resources