Update multiple rows at once Laravel Eloquent - laravel

I have the table products with the following structure.
id | name | promote
Where the column promote is of boolean type.
I want to set the value of the boolean column to 1 with the selected rows and set 0 to non-selected rows. I have the following code in the controller to handle this query.
$yes = Tour::whereIn('id', $request->promote)->get();
$no = Tour::whereNotIn('id', $request->promote)->get();
foreach ($yes as $item) {
$item->promote = 1;
$item->save();
}
foreach ($no as $item) {
$item->promote = 0;
$item->save();
}
I get following from the form request.
The above code does work but it isn't very efficient I assume. I'm looking for optional ways to achieve the result in a more efficient way.

Instead retrieving result, looping through, you can update directly,
$yes = Tour::whereIn('id', $request->promote)->update(['promote' => 1]);
$no = Tour::whereNotIn('id', $request->promote)->update(['promote' => 0]);

If you don't care about going through the Model to do the updating you can call update on the builder to update all the matched records. As this will use the builder and not the Model there will not be any model events fired:
// set them all to promote = 0
Tour::update(['promote' => 0]);
// or just set the ones that need to be 0
Tour::whereNotIn('id', $request->promote)->update(['promote' => 0]);
// set the ones you want to promote = 1
Tour::whereIn('id', $request->promote)->update(['promote' => 1]);
Just one way to give it a go.

Related

Put specific item on top without sorting others in laravel collection

I have an ordered laravel collection and i need too put element with id = 20 on top, without sorting other elements. Is it possible to do with sortBy?
You can try to use filter method
// Say $originalCollection is the response from the large request, with data from the database
$modifiedCollection = $originalCollection->filter(fn($item) => $item->id === 20)
->concat($originalCollection->filter(fn($item) => $item->id !== 20));
Or to be more intuitive you can use filter and reject methods
$modifiedCollection = $originalCollection->filter(fn($item) => $item->id === 20)
->concat($originalCollection->reject(fn($item) => $item->id === 20));
The $modifiedCollection will have record with id = 20 at the top and rest of the records will remain in the same order as in $originalCollection
if you want to put a specific item at the top of the array, simply add it separately.
$type = ['20' => 'Select Type'] + $your_sorted_array ;
Example:
$country = ['1' => 'Andorra'] + Countries::orderby('nicename')->pluck('name', 'id')->toArray();
Edit 1:Given new information, the on way you could "manually" do this is by using a combination of unset and unshift AFTER the array is built from the collection.
$key_value = $country[20];
unset($country[20]);
array_unshift($country, $key_value );
if your collection is not very large you can use combination of keyBy, pull and prepend methods
$originalCollection = Model::hereYourBigQuery()->get()->keyBy('id');
/*
now collection will look like this
{
'id1' => objectWithId1,
'id2' => objectWithId2,
...
20 => objectWithId20,
...
}
*/
// pull takes off element by its key
$toMakeFirst = $originalCollection->pull(20);
// prepend adding item into begining of the collection
// note that prepend will reindex collection so its keys will be set by default
$originalCollection->prepend($toMakeFirst);
upd:
if you want to stick with sort there is a way
$collection = Model::yourBigQuery()->get();
$sorted = $collection->sort(function($a, $b){return $a->id == 20 ? -1 : 1;})->values();
as said in docs method sort can take closure as argument and utilizes php uasort under the hood

Laravel boolean validation based on others column

I want to validate the is_default column based on domain_id. The condition is for every domain_id there can be only one(single) 1; others multiple will be 0 (zero).
I try to make a rule in Laravel that's like the following. But It's not working; I know it's wrong. So what would be the best query?
$query = Restaurant::where($attribute, $value)
->where('domain_user.domain_id', request()->get('domain_id'));
Here is something you might want:
function updateDefault($domain_id, $name) {
//updating default domain with given name
Restoraunt::where('domain_id',$domain_id)->where('name', $name)->first()->update(['is_default' => 1]);
//setting all other models with same domain_id to 0
Restoraunt::where('domain_id', $domain_id)->where('name', '!=', $name)->update(['is_default' => 0]);
}
If you for example use updateDefault(1, 'ABC - 2') results would be :

Eloquent to count with different status

It's possible to make one query to get total, sold & unsold in laravel eloquent?
$total_apple = Item::whereName('Apple')->count();
$sold_apple = Item::whereName('Apple')->whereStatus(2)->count();
$unsold_apple = Item::whereName('Apple')->whereStatus(1)->count();
Yes you can totally do that. You can use filter method on collection object returned by your Eloquent query.
$apples = Item::whereName('Apple')->get();
$soldApples = $apples->filter(function ($apple){
return $apple->status == 2;
});
$unsoldApples = $apples->filter(function ($apple){
return $apple->status == 1;
});
$soldApples and $unsoldApples contains the object of the items. You can then just use count($soldApples) and count($unsoldApples) to get their count.
filter method is against the collection object so there is no sql overhead.
There is no need run multiple queries or even fetch the entire results and use collection methods to loop through. Just use raw queries.
$apples = Item::whereName('Apple')
->selectRaw('COUNT(*) as total_apples,
SUM(status=2) as sold_apples,
SUM(status=1) as unsold_apples')
->first();
echo $apples->total_apples; // Outputs total apples
echo $apples->unsold_apples; // Outputs the unsold apples
echo $apples->sold_apples; // Outputs the sold apples
Since you are only doing simple counts though, you can use the query builder as well.
I would get all the items in one collection, then run the where statement on that collection. This should trigger a single Query.
$apples = Item::whereName('Apple')->get(); // This goes against SQL
$total_apple = $apples->count(); //This runs on the Collection object not SQL
$sold_apple = $apples->whereStatus(2)->count();
$unsold_apple = $apples->whereStatus(1)->count();

EF Code first - add collection to a collection, appropriate usage of skip() and take()

I have something like this:
var thread = _forumsDb.Threads
.Include("Posts")
.Single(t => t.Id == threadId);
Now, when i have a single thread, and a collection of posts within it, i want to count those posts, and then take some amount of them and remove the rest.
var count = thread.Posts.Count();
var tmp = thread.Posts.Skip(15).Take(15);
thread.Posts.Clear();
thread.Posts = tmp;
But that obviously doesn't work. So, how can i add collection to a collection? Is the thread.Posts.Clear(); appropriate here, or could i do it better?
In order to load only the 15 posts from the database you need and to perform only a single database query I would use a projection:
var data = _forumsDb.Threads
.Where(t => t.Id == threadId)
.Select(t => new
{
Thread = t,
Count = t.Posts.Count(),
Posts = t.Posts.OrderBy(p => p.SomeProperty).Take(15)
})
.Single();
var count = data.Count;
var thread = data.Thread;
Note that you need to order by some property if you want to use Take and Skip with LINQ to Entities. If in doubt just order by the post's Id.
If the relationship between Thread and Posts is one-to-many EF will populate the thread.Posts collection automatically with the 15 loaded posts.
This is how I would do it:
var count = thread.Posts.Count();
using (var tmp = thread.Posts.Skip(15).Take(15))
{
thread.Posts.Clear();
foreach (var Item in tmp)
thread.Posts.Add(Item);
}
You are skipping all the fifteen records which you are getting. Use below
Replace this
var tmp = thread.Posts.Skip(15).Take(15);
With
var tmp = thread.Posts.Skip(15).Take(30);
Hope this will help.

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

Resources