Unable to merge Laravel collections - laravel

I am not sure why this isn't working. I am having to make a series of paginated calls to an external API and want to combine all the pages into one master collection. Right now each call (page) returns its own collection. I know about the merge() method but for some unknown reason it isn't working.
Here is what I got:
$master_collection = collect();
for ($x = 1; $x <= $total_pages; $x++) {
$page = ApiService::getPage($x); //Returns collection of elements from the requested page
$master_collection->merge($page);
}
dd($master_collection);
However the dump just returns the empty collection the was initialized but doesn't have any of the page collections merged in. I do know the page collections work fine because I can save $page to a new index in an array and dd() the array and can see an array of the individual collections.

You will have to save the merged collection into the $master_collection variable.
$master_collection = collect();
for ($x = 1; $x <= $total_pages; $x++) {
$page = ApiService::getPage($x); //Returns collection of elements from the requested page
$master_collection = $master_collection->merge($page);
}

Related

Manual paginator on merged collection

I have a merged collection I want to paginate and I can't seem to figure this out.
First off, the reason I need to create the paginator manually is because I fetch 2 collections first and merge them, like so:
$a = ModelA::all();
$b = ModelB::all();
$c = $a->merge($b);
Next step is to paginate this collection, I tried the Paginator and the LengthAwarePaginator
PAGINATOR
$page = 1;
$results = new Paginator($c, 2, $page);
Here I get the 2 first results in a paginator
$page = 2;
$results = new Paginator($c, 2, $page);
I still get the 2 first results, while i'd expect the third and fourth result (the collection is more then 2 elements long!)
LENGTHAWAREPAGINATOR
$page = 1;
$results = new LengthAwarePaginator($c, count($c), 2, $page);
Here I get a Paginator but the items contain all elements of the collection, no matter what page number i ask for (instead of the 2 i'm asking for)
Any ideas on what might be the problem?
Thank you in advance
According to the Laravel community this is intentional.
You should slice your items before passing it to the Paginator.

Merging multiple objects which uses same id

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

CodeIgniter session shows different output in different functions

In first function of my controller, I am fetching random records from mysql table using CI active record
$query = $this->db->query("SELECT DISTINCT * FROM questions WHERE `level` = '1' ORDER BY RAND() limit 0,5");
$result = $query->result_array();
and saving result in session as
// saving questions id in session
for($i = 0; $i < count($result); $i++)
{
$session['questionsId'][] = $result[$i]['qId'];
}
$this->session->set_userdata($session);
and if print session variable it shows output like:
$qIds_o = $this->session->userdata('questionsId');
var_debug($qIds_o);
Array
(
[0] => 5
[1] => 9
[2] => 3
[3] => 6
[4] => 11
)
but if I retrieve same session in another function of same controller it shows different result
$qIds = $this->session->userdata('questionsId');
var_debug($qIds);
Array
(
[0] => 2
[1] => 8
[2] => 6
[3] => 3
[4] => 5
)
and if I remove ORDER BY RAND() from mysql query like:
$this->db->query("SELECT DISTINCT * FROM questions WHERE `level` = '1' limit 0,5");
it shows same session array in both functions. Very strange.
Please guide what is going wrong....
Here is my controller script:
public function set_value(){
$query = $this->db->query("SELECT DISTINCT * FROM questions WHERE `level` = '1' ORDER BY RAND() limit 0,5");
$result = $query->result_array();
// saving questions id in session
for($i = 0; $i < count($result); $i++)
{
$session['questionsId'][] = $result[$i]['qId'];
}
$this->session->set_userdata($session);
$qIds_o = $this->session->userdata('questionsId');
var_debug($qIds_o);
}
public function get_value(){
$qIds = $this->session->userdata('questionsId');
var_debug($qIds);
}
I called set_value() on page load while once the page loaded I call get_value() using AJAX post which simply hits my_controller/get_value/ and response back to browser.
Without looking at your controller (let's call it my_controller in detail, I think what might be happening is:
(1) You call the first function, my_controller/set_value where set_value sets the session variable and you echo the result.
(2) You then call the second function, show_value that simply echos out the session variable.
What you might be doing in set_value is:
1) echo out the current session variable
2) call the query and re-set the session variable
If this is the case, then when you go to show_value (2nd function) you are looking at the recently re-set value instead of the prior value that you echoed out in the first function.
I have two questions about this sentence:
but if I retrieve same session in another function of same controller it shows different result
Is this on a new page load?
If so, is the query run again?
I'm going to assume the answer to both of these questions is yes, since you said removing RAND() gives you the same results.
You are using a combination of RAND() and LIMIT in your query, meaning you want only five rows in a random order. That means that each time the query is run (and your session data is set), it is very likely that the results will be different.
I don't know exactly what you're doing with these IDs and what sort of data set you need, so this may not be 100% perfect for your solution, but if you only need to set this session data once, you should check if it exists before running the query.
if ($this->session->userdata('questionsId') === FALSE)
{
// Run your query and set your session data here.
// Note that in CI 3.0, Session::userdata()
// will return NULL if empty, not a boolean.
}

Magento - cms/page collection - apply filter to return only pages which are unique to a given store id (ie not also assigned to other stores)

I can use:
Mage::getModel('cms/page')->getCollection()->addStoreFilter($store_id);
to retrieve a collection of CMS pages filtered by Store Id.
But how do I get it to remove ones which are also assigned to other stores?
ie: I do not want it to return items which have 'All Store Views' as the Store View. (Or any other additional store id assigned to that CMS page.) It has to only return pages unique to that one store.
I am extending the Aitoc permissions module, so that Store Admins cant view or edit CMS pages and static blocks which might impact other stores. This involves filtering those items from the grid.
There's no native collection method to do this, so you'll need to
Query the cms_page_store table for pages unique to a given store
Use the results from above in your filter
I didn't fully test the following, but it should work (and if it doesn't, it'll give you a good start on your own query)
$page = Mage::getModel('cms/page');
$resource = $page->getResource();
$read = $resource->getReadConnection();
#$select = $read->query('SELECT page_id FROM ' . $resource->getTable('cms/page_store') . ' GROUP BY store_id');
//set total count to look for. 1 means the page only appears once.
$total_stores_count_to_look_for = '1';
//get the table name. Need to pass through getTable to ensure any prefix used is added
$table_name = $resource->getTable('cms/page_store');
//aggregate count select from the cmd_page_store database
//greater than 0 ensures the "all stores" pages aren't selected
$select = $read->query('SELECT page_id as total
FROM '.$table_name.'
WHERE store_id > 0
GROUP BY page_id
HAVING count(page_id) = ?',array($total_stores_count_to_look_for));
//fetch all the rows, which will be page ids
$ids = $select->fetchAll();
//query for pages using IDs from above
$pages = Mage::getModel('cms/page')->getCollection()->addFieldToFilter('page_id',array('in'=>$ids));
foreach($pages as $page)
{
var_dump($page->getData());
}
If you have thousands and thousands of CMS pages it may be worth it to alter the cms/page collection's select to join in aggregate table data. I'll leave that as an exercise for the reader, as those sorts of joins can get tricky.
$collection = Mage::getModel('cms/page')->getCollection();
$collection->getSelect()
->join(
array('cps' => $collection->getTable('cms/page_store')),
'cps.page_id = main_table.page_id AND cps.store_id != 0',
array('store_id')
)
->columns(array('stores_count' => new Zend_Db_Expr('COUNT(cps.store_id)')))
->group('main_table.page_id')
->having('stores_count = ?', 1)
->having('cps.store_id = ?', $storeId)
;
Fusing some elements of the solutions proposed by Alan and Vitaly with my own cumbersome understanding, I achieved what I needed with the following code.
To put into context, I was extending the Aitoc permissions module, so that Store Admins cant view or edit CMS pages and static blocks which might impact other stores. This involved filtering those items from the grid.
$collection = Mage::getModel('cms/page')->getCollection();
$collection->addStoreFilter(Mage::helper('aitpermissions')->getStoreIds());
$conn = Mage::getSingleton('core/resource')->getConnection('core_read');
$page_ids = array();
foreach($collection as $key=>$item) {
$page_id = $item->getId();
$results = $conn->fetchAll("SELECT * FROM cms_page_store
WHERE page_id = ".$page_id.";");
$count = 0;
$arr_stores = array();
foreach($results as $row) {
$arr_stores[] = $row['store_id'];
$count++;
}
//We dont want to show the item if any of the following are true:
//The store id = 0 (Means its assigned to All Stores)
//There is more than one store assigned to this CMS page
if( in_array('0',$arr_stores) || $count>1) {
//This removes results from the grid (but oddly not the paging)
$collection->removeItemByKey($key);
}
else {
//build an array which we will use to remove results from the paging
$page_ids[] = $page_id;
}
}
//This removes results from paging (but not the grid)
$collection->addFieldToFilter('page_id',array('in'=>$page_ids));
I'm not sure why I needed to use two different methods to filter from the paging and the grid.
The site uses magento 1.5 so perhaps there is an issue related to that.
Either way, this solution worked for me.
My solution to add field store_id to pages collection via join, and use collection method addFieldToFilter().
$pages = Mage::getModel('cms/page')->getCollection();
$pages->getSelect()->joinInner(
array('cms_page_store' => 'cms_page_store'),
'main_table.page_id = cms_page_store.page_id',
array()
);
$pages->addFieldToFilter('store_id', ['in' => [1, 2]]);

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