Query a relationship that is NOT part of a scope in Laravel - laravel

For a model I have a complicated scope condition like so:
class Foo {
public function scopeActive(Builder $query) {
$dateNow = $now->format('Y-m-d');
$timeNow = $now->second(0)->format('H:i:s');
$query->whereNull('start_date')
->orWhere('start_date', '<', $dateNow)
->orWhere(function (Builder $query) use ($dateNow, $timeNow) {
$query->where('start_date', '=', $dateNow)
->where('start_time', '>=', $timeNow);
});
}
}
This complicated condition will select all the records in Foo that are considered active (the real scope is even more complicated than that).
I have another class like so:
class Bar {
public function foos() {
return $this->hasMany(Foo::class);
}
}
Which means the Bar model has many Foo models.
Now if I wanted to get all the Bar models as well as all the active Foo models that belong to it, I can do the following:
Bar::with(['foo', function (HasMany $query) {
$query->active();
})->get();
However, how can I write a query that gives me all the Bar records that are NOT active.
Ideally I would want something like this:
Bar::with(['foo', function (HasMany $query) {
$query->whereNot(function (Builder $query) {
$query->active();
});
})->get();

If you want to have a scope that does the opposite, just create another scope method like so, but with inverted query logic (you didn't include your 'Some complicated query', so this is just a guess):
public function scopeInactive($query) {
return $query->where('is_active', false);
}
Laravel: local scopes

There does not seem to be an efficient way to easily invert SQL using Laravel for complicated queries.
You just gotta have to write the inverse.

Related

How to improve multiple eloquent conditions?

Here is a default eloquent condition:
if($request->has("parent")) => {
Specializatio::where('id', $request->id)
->when($request->parentid, function($query) use ($request){
return $query->where('parent_id', $request->parent_id);
})
}
It grows up so fast into long list of conditions .when based request object. Is there any flexibale mechanism to manage it, maybe devide in another class, helper?
Maybe query scopes and short hand functions will make your code a bit cleaner.
class Specialization extends Model
{
public function scopeParent($query, $parentId)
{
return $query->where('parent_id', $parentId);
}
}
Specialization::whereKey($request->id)
->when($request->filled('parent_id'), fn ($query) => $query->parent($request->parent_id));

Laravel Controller - how to get Model's query object directly?

The below Controller method changes the query based on the flags which are activated.
public function index(Request $request)
{
$q = new ProductModel();
if($request->has('flag1')) {
$q = $q->includeFlag1();
}
if($request->has('flag2')) {
$q = $q->doFlag2();
}
if($request->has('flag3')) {
$q = $q->doFlagthing3();
}
return $q->paginate();
}
Most example code I've seen will call a where() from the beginning instead of creating a new Model instance and looks something like this:
$q = ProductModel::where('available', true);
if($request->has('flag1')) {
$q->includeFlag1();
}
But in my case based on the table fields it isn't possible for me to start from a where like this so it seems I must do $q = $q every time in every case... It's not neat, neither would doing something hacky like attempting to use a where true clause.
How can I clean either get the query object neatly from the beginning and use that or otherwise avoid having to do $q = $q inside each if()?
You can use query() and when() methods :
public function index(Request $request)
{
$query = ProductModel::query()
->when($request->has('flag1'), function ($q) {
$q->includeFlag1();
})
->when($request->has('flag2'), function ($q) {
$q->doFlag2();
})
->when($request->has('flag3'), function ($q) {
$q->doFlagthing3();
})
->paginate();
}
The official documentation is here: https://laravel.com/docs/9.x/queries#conditional-clauses
Sometimes you may want certain query clauses to apply to a query based on another condition. (...) The when method only executes the given closure when the first argument is true. If the first argument is false, the closure will not be executed.
You have also a more concise alternative using arrow functions (thanks #miken32's comment):
public function index(Request $request)
{
$query = ProductModel::query()
->when($request->has('flag1'), fn ($q) => $q->includeFlag1())
->when($request->has('flag2'), fn ($q) => $q->doFlag2())
->when($request->has('flag3'), fn ($q) => $q->doFlagthing3())
->paginate();
}

Laravel prioritize or operator in relation query (hasMany). How to add parenthesis?

The events method in my model returns all related events from the database. The code below is working fine, the only problem is dat it don't prioritze the or. (See orWhereHas)
public function events()
{
return $this->hasMany(Event::class)
->orWhereHas('organisations', function(Builder $query){
$query->where('organisation_id', $this->id);
});
}
When I extend the query somewhere else in the code, it goes wrong:
$model->events()->whereNull('some_field')
Because it should prioritize the OR operator. But I don't know how to do that in this case because I am imitating the query from a model relation.
So the question is: how to add parenthesis in the query to prioritize the or operator?
You could move the logic outside the relationship method and use a where/orWhere Closure.
public function events()
{
return $this->hasMany(Event::class)
}
$model->events()
->where(function ($sub) {
$sub->orWhereHas('organisations', function(Builder $query){
$query->where('organisation_id', $this->id);
})
->orWhereNull('some_field');
})

Best way to query model relationship in Eloquent

I am working within a controller in a Laravel application. I am returning a table to the view. The table is based on my PlanSubmission model. I am receiving parameters through a GET request and using those parameters to return a filtered set of rows to my view.
The first part of my controller looks like this and is working fine:
public function index()
{
//Used for filter. The request is received in the URL
if (request()->has('status')) {
$plans = PlanSubmission::where('status', request('status'))->paginate(25)->appends('status', request('status'));
}
elseif (request('employer_name')) {
$plans = PlanSubmission::where('employer_name', request('employer_name'))->paginate(25)->appends('employer_name', request('employer_name'));
}
I have run into a problem because now I need to use a model relationship in the controller. I am receiving 'advisor_name' from the request. The 'advisor_id" column is the foreign key on the PlanSubmission model. The 'advisor_name' column exists in the Advisor model. I have a function on my PlanSubmission model that looks like this:
public function advisor()
{
return $this->belongsTo(Advisor::class);
}
Initially, I thought there was a way I could do this easily with something like:
$plans = PlanSubmission::where(advisor->name, request('advisor_name'))->paginate(25)->appends('advisor_name', request('advisor_name'));
Of course, this will not work because I cannot enter a relationship into the first parameter in the Where Clause.
I do not know where to go from here. My other thought is to return all the advisors first from the Advisor model like this:
$advisors = Advisor::where('name', request('advisor_name'));
Then, I imagine I would have to somehow loop through that and get the id (primary key) for each of the objects in $advisors and somehow get that into the PlanSubmission where clause. I'm totally lost.
Like Victor mentions in his answer you can use whereHas like so:
PlanSubmission::whereHas('advisor', function ($query) {
$query->where('name', request('advisor_name'));
});
You didn't asked this directly, but I noticed that you use conditionals to make different queries. Eloquent provides a few way to make this a bit nicer to deal with.
The first which is kind of obvious is that that whatever method you call a builder (query) is returned that you can just add on to. It could be there were some common restrictions in your two cases:
public function index()
{
$query = PlanSubmission::where('something', 42);
if (request()->has('status')) {
$query = $query->where('status', request('status'));
} elseif (..) {
...
}
return $query->paginate(25);
}
Another way to do conditional queries in Laravel is using when. E.g. for status:
$query = $query->when(request->has('status'), function ($query) {
// note that you don't have to return the query
$query->where('status', request('status'));
});
// or PlanSubmission::>when(..)
In your example you cannot both filter by status AND advisor_name, but lets assume that would be okay, then you can combine everything like so:
public function index()
{
return PlanSubmission::query()
//->where('something', 42)
->when(request->has('status'), function ($query) {
$query->where('status', request('status'));
})
->when(request->has('advisor_name'), function ($query) {
$query->whereHas('advisor', function ($query) {
$query->where('name', request('advisor_name'));
});
})->paginate(25);
}
This approach may seem verbose for simple queries and then it is fine to use if conditions, but for complex queries when can be useful. Also the idea of "building up a query" also works nice in those situation. You can pass the query builder around and continuously build it up.
You can use whereHas for that
docs

Eloquent `with()` with filtering based on relation

I have this tables.
And this model relations, this relations works fine.
class Item extends Model
{
public function translations()
{
return $this->hasMany(ItemTranslations::class);
}
}
class ItemTranslation extends Model
{
public function language()
{
return $this->belongsTo(Language::class);
}
}
I need to return a list of items with the translations, but only the translations related to a specific language.
I can't have this query working, im getting all translations of each item, not only the one filtered with this query. The language related to the translation is not needed on the result.
$query = Item::query();
$query->with('translations')->when('language',function($query) use ($ISOlanguage) {
return $query->where('languages.ISO_code', '=', $ISOlanguage);
});
return $query->paginate();
Any idea who i can have this working? Thanks!
So what you want to do is constraining eager loading
Item::with(["translations" => function ($query) use ($ISOlanguage) {
$query->where('language.ISO_code', $ISOlanguage);
}])->get();
https://laravel.com/docs/5.8/eloquent-relationships#constraining-eager-loads
I finally have it working
Item::with(['translations' => function($query) use ($ISOlanguage) {
$query->whereHas('language', function($query) use ($ISOlanguage) {
$query->where('ISO_code', '=', $ISOlanguage);
});
}])->get();
Thanks #julian-s for your help!

Resources