How do I alter the query builder everytime I'm getting a new model?
I've found that overriding the method below works, but I'm not feeling good doing this. Is there a better way to do this?
So I want ->join() to be executed everytime for a specific model.
!! I don't want to use the protected $with = []; property, cause I don't want extra queries to be executed when not necessary.
public function newQueryWithoutScopes()
{
$builder = $this->newEloquentBuilder(
$this->newBaseQueryBuilder()
);
// Once we have the query builders, we will set the model instances so the
// builder can easily access any information it may need from the model
// while it is constructing and executing various queries against it.
return $builder->setModel($this)->join('join statement comes here')->with($this->with);
}
To affect the query every time a model is used, you can use the newQuery method.
public function newQuery($excludeDeleted = true){
return parent::newQuery()->join('orders', 'users.id', '=', 'orders.user_id');
}
But this will probably break Eloquent since your Model isn't representative of your database model anymore; it is now a Frankenmodel of two separate models.
Related
I have a Product and a Tag model. They are connected with a many-to-many relation.
When I get a collection of Tag, I want it to be sorted at any time. Because the sort algorithm is implemented in PHP, I can not do this on database-level.
My first attempt was to simply override the newCollection() method of Model:
// Tag.php
public function newCollection(array $models = []): Collection
{
return parent::newCollection($models)->sortBy(fn(Tag $tag) => $tag->present()->name)->values();
}
That works. Unfortunately, newCollection() is always called twice when retrieving a Tag collection from the database:
First time: Builder::hydrate()
Second time: Builder::get()
That's far away from efficient. So I made another attempt. Instead of overriding newCollection(), I've overridden the newEloquentBuilder() method of Model:
// Tag.php
public function newEloquentBuilder($query): Builder
{
return new class($query) extends Builder {
public function get($columns = ['*'])
{
return parent::get($columns)->sortBy(fn(Tag $tag) => $tag->present()->name)->values();
}
};
}
Seems to work! But unfortunately, not every time. $product->tags won't use Builder::get(). It uses BelongsToMany::get().
So here my question: Is there an efficient way to modify a collection of a specific model after it has been retrieved from the database?
It's often a bad idea to modify the default behavior like this, and you often find yourself needing the original default behavior somewhere down the road. I think a much cleaner approach would be to add a second method to the model which retrieves the collection and then applies the sort method.
public function sortedTags(): Collection {
return $this->tags->sortBy('tag.present.name');
}
You may need to eager load the relationships to optimize things.
All that said, this is a bad idea from a performance perspective. It loads all of the data and then loops through it to sort. If you can push the sorting/ordering into sql you will be better off in the long run, especially if the set of data in the Tags table is expected to grow with time. It certainly makes for a less clean query as you would probably need to add the joins in to get things working, and you might want to switch to query builder depending on the complexity.
My application supports fetching data with filters. My current implementation (which works fine) is
Model::select($fields)->with($relations)->tap(function ($query) use ($filters) {
// A lot of filtering logic here
// $query->where()......
})->get();
However, I would like to move the filtering logic directly into the Model so I could just do
Model::select($fields)
->with($relations)
->applyFilters($filters)
->get();
I have tried to add a filter method to the Model but at that point I'm working with a Builder and it does not recognize my function:
Call to undefined method Illuminate\Database\Eloquent\Builder::applyFilters()
Is there an easier way to do this, other than creating a new builder class and use that?
I figured it out! I just had to add a scopeApplyFilters to my Model class. It injects the Builder as the first parameter automatically, so the logic ends up looking like
public function scopeApplyFilters($query, $filters)
{
// Perform filtering logic with $query->where(...);
return $query;
}
Then I can just call it with Model::applyFilters($filters);
I've inherited a Laravel 5 project at work and would like to know how I should check for the existence of a related model to avoid null exceptions.
BlockDate model
class BlockDate extends Model {
public function claims()
{
return $this->belongsToMany(User::class);
}
}
User model
class User extends Model {
public function blocks()
{
return $this->belongsToMany(BlockDate::class);
}
}
Pivot table
$table->unsignedInteger('user_id');
$table->foreign('user_id')->references('id')->on('users');
$table->unsignedInteger(block_date_id');
$table->foreign('block_date_id')->references('id')->on(block_dates);
Users can claim a range of dates for vacation requests. However, users may have no claims or dates may not have been claimed. I am currently using
if ($user->blocks->count() > 0) {
$dates = $user->blocks->sortByDesc('created_at');
// more logic here....
}
I do not like using count everywhere, is there a way to incorporate the check like:
// I don't know what hasClaimedDates might be
$dates = $user->hasClaimedDates()->blocks->sortByDesc('created_at');
You can use the actual relationship method instead of the magic accessor:
$sortedDates = $user->blocks()->latest()->get();
This will give you an empty collection if no relations are established, but it will not fail on the sorting.
Note: latest() is an equivalent for orderBy('created_at', 'desc') in this case.
By the way, if you use $user->blocks->count(), it will first load all related models into memory and then count on the relation. If you are going to use the related models afterwards, that is fine. But if you don't and you only count them, this is a waste of resources. In this case $user->blocks()->count() is way more performant as it executes a database query that only returns a single number. Take this into consideration especially where you have a lot of related models.
Laravel offers an optional helper method to guard against nulls:
// will return either a collection or null
$dates = optional($user->blocks)->sortByDesc('created_at');
I'm confused about the best way to get data from DB. I have this controller (Task) that get from Task model the tasks of each customers. What's the best way to get these data?
1° Example
In this example I have a "general" function (getTasksCompany) that join the tables (Task and Companies). The showTasks call this function and then use where clause for get only tasks with customer code = 000001
public function showTasks() {
$tasks = $this->getTasksCompany()->where("company_code", "=", "000001")->get();
dd($tasks);
}
public function getTasksCompany() {
$tasks = Task::join("companies AS c", "c.code", "=", "company_code");
return $tasks;
}
2° Example
In this example I have a "specific" function that get tasks from the code in the passed as argument.
public function showTasks2() {
$tasks = $this->getTasksFromCompany("000001");
dd($tasks);
}
public function getTasksFromCompany($company_code) {
$tasks = Task::join("companies AS c", "c.code", "=", "company_code")->where("company_code", "=", $company_code)->get();
return $tasks;
}
3° Example
In this example I have a "general" function (getTasksCompany) that join the tables (Task and Companies) and I use the scope defined from Task model to filter the tasks.
public function showTasks3() {
$tasks = $this->getTasksCompany()->company("000001")->get();
dd($tasks);
}
public function getTasksCompany() {
$tasks = Task::join("companies AS c", "c.code", "=", "company_code");
return $tasks;
}
public function scopeCompany($query, $company_code) {
return $query->where("company_code", "=", $company_code);
}
My question is, what's the good practice to do? And Why?
Based on my understanding, asking for best practices would attract opinionated answer but generally because you use Laravel, I would try as much as possible to make use of the functionalities it provide.
While I would prefer the third example more than the others because using model scope helps to create and bind query builder from an instance of the model. This would make things easier when you reuse this function.
This means you don't need to statically call any query builder method since they bind to the initial model in the first place.
An example if I would do the above I would simply employ Model Relationship that would handle my joins under the hood:
//Company model
public function scopeShowTask($company_code = "000001")
{
return $this->tasks()->where("company_code", "=", $company_code);
}
public function tasks()
{
return $this->hasMany(Task::class, 'company_code', 'code');
}
Using this method helps to construct your query based on the relationship between Task and Company. In order to understand how this works, you should check out Eloquent Relationship
One great advantage of using this method is that you can easily take advantage of the various method laravel provides when you declare a relationship in your model this way. To see some of them, you can check out Querying relationship
PS: Best practice, no, maybe just a better practice given the situation. This answer is open to an update
I suggest that you study eloquent and query builder thoroughly and that's where the best practice is.
If you use eloquent with query builder properly you won't need another function in order fetch the data that you want.
https://laravel.com/docs/5.5/queries
https://laravel.com/docs/5.5/eloquent
My Association model looks like this (irrelevant code redacted):
class Association extends Model
{
public function members() {
return $this->hasMany('App\Member');
}
}
My Member model looks like this:
class Member extends Model
{
public function scopeActive($query) {
return $query->where('membership_ended_at', Null);
}
public function scopeInactive($query) {
return $query->whereNotNull('membership_ended_at');
}
}
This is what I want to be able to do:
$association = Association::find(49);
$association->members->active()->count();
Now, I'm aware there's a difference between a Query and a Collection. But what I'm basically asking is if there's some kind of similar scope for collections. Of course, the optimal solution would be to not have to write TWO active methods, but use one for both purposes.
(question already answered in the comments, but might as well write a proper answer)
It is not possible to use a query scope in a Colletion, since query scope is a concept used in Eloquent to add constraints to a database query while Collections are just a collection of things (data, objects, etc).
In your case, what you need to do is to change this line:
$association->members->active()->count();
to:
$association->members()->active()->count();
This works because when we call members as a method, we are getting a QueryBuilder instance, and with that we can start chaining scopes to the query before calling the count method.