Overwrite paginator items in Laravel - php

I have a list of ids that come from a complicated query. I paginate the response of that complicated query and then use those ids to get the eloquent models. I then put it through a resource with the pagination meta data.
The laravel AbstractPaginator class protects the items attribute so you cannot easily overwrite them. I have a solution to use a ReflectionProperty but I'm after a simpler solution.
The below works but it is not particularly elegant.
// $studentIds == Long complicated query that would return 1000s of students
$data = $studentIds->paginate(); // Execute the query limited to 15.
// Use ids to get eloquent models for our students
$students = Student::whereIn('id', $data->pluck('id'))->get();
// Overwrite paginate `items` attribute so that our response contains pagination meta.
$rp = new \ReflectionProperty('Illuminate\Pagination\AbstractPaginator', 'items');
$rp->setAccessible(true);
$rp->setValue($data, $students);
return new StudentResourceCollection($data);

Use the setCollection() method:
$data->setCollection($students);

Related

How to use append attributes with Laravel Eloquent and pagination

How can I use append attributes with Laravel Eloquent and pagination? We are trying to load the data from Laravel Eloquent, wherein we have to append attributes. So we want to query based on the append attributes, which works fine when not using skip and take. But we need to add the pagination and need the option of skip and take, but return an error.
Model
protected $appends = ['isSlaBreach'];
public function getIsSlaBreachAttribute()
{
return true or false
}
Controller
$overdue_tickets->skip($skip);
$overdue_tickets->take($take);
$res = $overdue_tickets->get()->where('isSlaBreach', true);
Need guidance on the same.
but which error are you getting?
Note that querying over appended attributes it is not possible using directly the db: you are able to query over appended attributes by using Eloquent Collections.
I think in your code there are some problems:
1 - in the Model return true or false returns always true
2 - In the controller you should add a final all() in order to get filtered elements from the collection:
$res = $overdue_tickets->get()->where('isSlaBreach',true)->all();

Laravel - Convert array back to a collection

I am trying to get a collection of categories only if they are in the products table. My categories table has 300 items. I only want a collection if a category is attached in the products table. The $categories collection should only result in about 10 categories because there are only about 10 products that have different category_ids
$products = DB::table('products')->groupBy('category_id')->get();
foreach($products as $product){
$categories[] = DB::table('categories')->where('id', '=', $product->category_id)->first();
}
$categories = collect($categories)->all();
Maybe I am going about this wrong and should use a different query builder method?
The end result $categories does get the results I am after but in my blade I get the "Trying to get property of non-object" error.
If using $categories in your blade file as a collection, you will need to remove the ->all() method.
->all() is converting the collection into an array after it is being created:
$categories = collect($categories);
You get Trying to get property of non-object because one of
DB::table('categories')->where('id', '=', $product->category_id)->first();
return null value.
You can fix it this way
$products = DB::table('products')->groupBy('category_id')->get();
$categories = collect();
foreach($products as $product){
$category = DB::table('categories')->where('id', '=', $product->category_id)->first();
if ($category) {
$categories->push($category);
}
}
If you want to get collection instance you must be use collect() helper method with array argument.
For example
collect($categories); // is_array(categories) is true
You are doing many request in foreach. That is not right way. Instead it you can achieve collection instance doing only 2 request.
$categoryIds = DB::table('products')->pluck('category_id')->toArray();
$categories = DB::table('categories')->whereIn('id', $categoryIds)->get();
See docs https://laravel.com/docs/5.8/queries#retrieving-results
This can be done with one simple eloquent query. There's no need to use query builder unless you're doing something overly complex (in my opinion).
whereHas() will only return Categories that have Products.
Categories::with('products')->whereHas('products')->get();
As long as the relationships are correct on the models the above is what you're looking for. As pointed out in the comments, you need models and relationships. Laravel uses the MVC pattern, and the first letter stands for model so I'm going to guess you're using them. If not let me know and I can help set those up because you should be using them.
And if you NEED to use the query builder clean that code up and use something like this so you don't have to worry about recollecting. Also check out the hydrate() method to change these generic class instances into instances of the Categories model.
DB::table('categories')->whereIn('id', function($q){
$q->select('category_id')->from('products');
})->get();

Cant retrieve record from large scale table row

I am using laravel framework.In my database records table have more then 400000 row. Now I want to retrieve record using some condition
logic :
need to match service id
need to match status
but my application cant retrieve data (unable to handle this request).
I am using query using this
foreach ($ven as $ven){
$data = $ven->data;
$record = $data ->records()->where('status','success')->get();
}
My status column already added in index.
Need suggestion
First you need to keep statuses in int form not as strings. It would help you in filtering records easily.
And use chunk for handling large datasets. I suppose $ven is for venue. So, follow the code below.
DB::table('venues')
->select('venues.*', 'records.*')
->join('records', 'venues.id', '=', 'records.venue_id')
->orderBy('id')->chunk(1000, function($venues) {
foreach ($venues as $venue) {
// your logic
}
});
Note: I used query builder instead eloquent. That is because query builder is faster than eloquent.
The way you are trying to access related records in loop could create N + 1 problem
As per official docs When accessing Eloquent relationships as properties, the relationship data is "lazy loaded". This means the relationship data is not actually loaded until you first access the property. However, Eloquent can "eager load" relationships at the time you query the parent model. Eager loading alleviates the N + 1 query problem
You could eager load your related data as
$results = ParentModel::with('records')
->where(...) /* If there are any filters for ParentModel */
->get();
For eager loading only 2 queries will be executed to get your data for main model and related model. Each row in $results will a collection of related records which you can iterate to list details of each related object.
To get filtered rows of related model you could modify with() method as
$results = ParentModel::with(['records' => function ($query) {
$query->where('status','=','success');
}])
->where(...) /* If there are any filters for ParentModel */
->get();
To get filtered rows of ParentModel on basis of related records you could use whereHas
$results = ParentModel::with('records')
->where(...) /* If there are any filters for ParentModel */
->whereHas('records', function ($query) {
$query->where('status','=','success');
})->get();

Laravel exclude current id from query eloquent results

I am fairly new to laravel and I built a little "similar posts" section. So every post has a tag and I query all the id's from the current tag. And then I find all the posts with thoses id's. Now my problem is that the current post is always included. Is there an easy way to exclude the current id when querying?
I can't seem to find anything in the helper function on the laravel docs site
this is my function:
public function show($id)
{
$project = Project::findOrFail($id);
foreach ($project->tags as $tag){
$theTag = $tag->name;
}
$tag_ids = DB::table('tags')
->where('name', "=", $theTag)
->value('id');
$similarProjects = Tag::find($tag_ids)->projects;
return view('projects.show', ['project' => $project, 'similarProjects' => $similarProjects]);
}
An easy way to solve your issue would be to use the Relationship method directly instead of referring to it by property, which you can add additional filters just like any eloquent transaction.
In other words, you would need to replace this:
Tag::find($tag_ids)->projects
With this:
Tag::find($tag_ids)->projects()->where('id', '!=', $id)->get()
Where $id is the current project's id. The reason behind this is that by using the method projects(), you are referring your model's defined Relationship directly (most probably a BelongsToMany, judging by your code) which can be used as a Query Builder (just as any model instance extending laravel's own Eloquent\Model).
You can find more information about laravel relationships and how the Query Builder works here:
https://laravel.com/docs/5.1/eloquent-relationships
https://laravel.com/docs/5.1/queries
However, the way you are handling it might cause some issues along the way.
From your code i can assume that the relationship between Project and Tag is a many to many relationship, which can cause duplicate results for projects sharing more than 1 tag (just as stated by user Ohgodwhy).
In this type of cases is better to use laravel's whereHas() method, which lets you filter your results based on a condition from your model's relation directly (you can find more info on how it works on the link i provided for eloquent-relationships). You would have to do the following:
// Array containing the current post tags
$tagIds = [...];
// Fetch all Projects that have tags corresponding to the defined array
Project::whereHas('tags', function($query) use ($tagIds) {
$query->whereIn('id', $tagIds);
})->where('id', !=, $postId)->get();
That way you can exclude your current Project while avoiding any duplicates in your result.
I don't think that Tag::find($tag_ids)->projects is a good way to go about this. The reason being is that multiple tags may belong to a project and you will end up getting back tons of project queries that are duplicates, resulting in poor performance.
Instead, you should be finding all projects that are not the existing project. That's easy.
$related_projects = Project::whereNotIn('id', [$project->id])->with('tags')->get();
Also you could improve your code by using Dependency Injection and Route Model Binding to ensure that the Model is provided to you automagically, instead of querying for it yourself.
public function show(Project $project)
Then change your route to something like this (replacing your controller name with whatever your controller is:
Route::get('/projects/{project}', 'ProjectController#show');
Now your $project will always be available within the show function and you only need to include tags (which was performed in the "with" statement above)

Eloquent - Updating all models in a collection

I want to set a certain attribute in all the models of a collection.
in plain SQL:
UPDATE table SET att = 'foo' WHERE id in (1,2,3)
the code i have:
$models = MyModel::findMany([1,2,3]);
$models->update(['att'=>'foo']);
taken from here
but doesn't work. I'm getting
Call to undefined method Illuminate\Database\Eloquent\Collection::update()
the only way i have found it's building a query with the query builder but i'd rather avoid that.
You are returning a collection, not keeping the query open to update. Like your example is doing.
$models = MyModel::whereIn('id',[1,2,3]);
$models->update(['att'=>'foo']);
whereIn will query a column in your case id, the second parameter is an array of the ids you want to return, but will not execute the query. The findMany you were using was executing it thus returning a Collection of models.
If you need to get the model to use for something else you can do $collection = $models->get(); and it will return a collection of the models.
If you do not just simply write it on one line like so;
MyModel::whereIn('id',[1,2,3])->update(['att'=>'foo']);
Another option which i do not recommend is using the following;
$models = MyModel::findMany([1,2,3]);
$models->each(function ($item){
$item->update(['att'=>'foo']);
});
This will loop over all the items in the collection and update them individually. But I recommend the whereIn method.
The best solution in one single query is still:
MyModel::whereIn('id',[1,2,3])->update(['att'=>'foo']);
If you already have a collection of models and you want to do a direct update you can use modelKeys() method. Consider that after making this update your $models collection remains outdated and you may need to refresh it:
MyModel::whereIn('id', $models->modelKeys())->update(['att'=>'foo']);
$models = MyModel::findMany($models->modelKeys());
The next example I will not recommend because for every item of your $models collection a new extra query is performed:
$models->each(function ($item) {
$item->update(['att'=>'foo']);
});
or simpler, from Laravel 5.4 you can do $models->each->update(['att'=>'foo']);
However, the last example (and only the last) is good when you want to trigger some model events like saving, saved, updating, updated. Other presented solutions are touching direct the database but models are not waked up.
Just use the following:
MyModel::query()->update([
"att" => "foo"
]);
Be mindful that batch updating models won't fire callback updating and updated events. If you need those to be fired, you have to execute each update separately, for example like so (assuming $models is a collection of models):
$models->each(fn($model) => $model->update(['att'=>'foo']) );

Categories