laravel filter collection based on relation - php

Assuming the following code to represent an order and the related transactions:
order
public function transactions(){
return $this->hasMany('App\Transaction');
}
already loaded collection ($orders)
order1
id
amount
transactions (relation)
txid
method
amount
order2
id
amount
transactions (relation)
txid
method
amount
The following filtering on already loaded collection does not work as expected:
$isNotEmpty = $orders->filter(function ($order) use ($receivingPayment) {
return $order->transactions->txid === $receivingPayment->txid && $order->transactions->method === $receivingPayment->method;
})->isNotEmpty();
It seems like the filtering on the relation transactions doesn't work this way?
It returns an empty element even if the transaction id is in the collection.

If you can't or don't want to use the answers above and keep working with the collection you've got, use a combination of filter and pluck:
$orders->filter(function ($order) use ($receivingPayment) {
return $order->transactions
->pluck('id')
->containsStrict($receivingPayment->txid);
})
To filter for multiple conditions matching a single transaction, use a combination of multiple where's and isNotEmpty():
$orders->filter(function ($order) use ($receivingPayment) {
return $order->transactions
->where('txid', $receivingPayment->txid)
->where('method', $receivingPayment->method)
->isNotEmpty();
})

Instead of filtering orders, you wanted to filter transactions. You can do that like this:
$orders->transactions()->where('transactions.txid', '=', $receivingPayment->txid)->get();

You should try whereHas instead.
$orders->whereHas('transactions', function($query) use ($receivingPayment) { $query->where('txid', '=' $receivingPayment->txid})->count();
If there is nothing found, than you should doubleCheck if there no missmatch with $receivingPayment id in database.

Related

laravel collection nested "where"

Assume the following collection:
[
order
-id
-receiverAddress
-...
relations
transactions
-id
-transaction_id_external
- ...
]
I tried to use the following filter on the collection:
$isNotEmpty = $orders->filter(function ($order) use ($receivedPaymentDetails) {
return $order->transactions
->where('transaction_id_external', $receivedPaymentDetails->txid)
->where('order.receiverAddress', $receivedPaymentDetails->address)
->isNotEmpty();
})->isNotEmpty();
It seems like this doesn't work, any idea how I can filter on the parent collection item order.receiverAddress?
Since you want to filter based on the transactions relations, you must perform some form of join query with your transactions table
Easiest way for you would be to place your queries in a whereHas callback
$isNotEmpty = $orders->filter(function ($order) use ($receivedPaymentDetails) {
return $order->whereHas('transactions', function ($query) use ($receivedPaymentDetails) {
$query->where('transactions.transaction_id_external', $receivedPaymentDetails->txid);
$query->where('order.receiverAddress', $receivedPaymentDetails->address);
})
->isNotEmpty();
})->isNotEmpty();

With PHP Laravel, how can I make an Eloquent query that is conditional like a "when" statement, but dependent on the value of retrieved columns?

This question can probably be best asked in the form of an analogy.
Let's say I have a "table" model that has many "filters" - these filters have a column "type" which has to either be "color" or "make", and an "allowed" column which contains the array of allowed values for each. I want to use these tables to display a filtered list of "cars" (which have color/make columns), and if I then add a car to the DB, I want it to figure out the set of tables to which that car needs to be added depending on every table's filters for car and make. So that means I query tables, and I join filters - but where the filters are "color", they have to contain the color of the car, and where the filters are "make", they have to match the make of the car. In this way, the table would get a list of cars that matches all of its filters. In pseudocode, this would be something like:
Table::join('filters', 'filters.table_id', '=', 'tables.id')
->when('filters.type', '=', "color", function($query) use ($car) {
$query->whereJsonContains('filters.allowed', $car->type);
})
->orWhen('filters.type', '=', "make", function($query) use ($car) {
$query->whereJsonContains('filters.allowed', $car->make);
})
->get();
And I'm trying to work out what, if possible, the correct way of writing such a query would be. I had the following before:
Table::join('filters', 'filters.table_id', '=', 'tables.id')
>where(function ($query) use ($car) {
$query->where('filters.type', "color")
->whereJsonContains('filters.allowed', $car->color);
})->orWhere(function($query) use ($car) {
$query->where('filters.type', "make")
->whereJsonContains('filters.allowed', $car->make);
})->get();
But this would return all tables where the car matched any of the filters instead of all of them. If I make it a where instead of an orWhere, then I get conflicting conditions in where('filters.filter_type', "make") and where('filters.filter_type', "color"), which will then give me no results at all. So, is it possible to write conditional when clauses that depend upon the value of columns like in my pseudocode example? Thanks very much!
Figured it out! Wasn't actually as difficult as I thought, and didn't require any raw SQL. First, I created two extra relationships for the table in addition to the filters one:
public function filters()
{
return $this->hasMany(Filter::class);
}
public function colorFilters()
{
return $this->filters()->where('type', "color");
}
public function makeFilters()
{
return $this->filters()->where('type', "make");
}
Then I was able to make the query work by using whereHas and logical grouping:
Table::where(function ($query) use ($car) {
$query->whereHas('colorFilters', function($colorQuery) use ($car) {
$colorQuery->whereJsonContains('allowed', $car->color);
})
->orWhereDoesntHave('colorFilters');
})
->where(function ($query) use ($car) {
$query->whereHas('makeFilters', function($makeQuery) use ($car) {
$makeQuery->whereJsonContains('allowed', $car->make);
})
->orWhereDoesntHave('makeFilters');
})
->get();

Laravel - Property does not exist on this collection instance

On my Users model I have a query that gives me "Property [manager_id] does not exist on this collection instance" error. I suppose this is because the collection of expiringUsers does not have a single property manager_id, but I'm not sure how to loop through expiringUsers in the query.
What I'm trying to return is a collection of managers where their ID matches the expiring manager_id.
public function scopeManagers()
{
$expiringUsers = $this->ExpiringContractors(); // This returns a collection of users
return $mangers = $this->where(function($query) use ($expiringUsers){
$query->where('id', $expiringUsers->manager_id);
})->get();
}
You already said it yourself... If $expiringUsers is a Collection, you can't access manager_id without looping, but you actually don't have to in this case. If you want to query id for those that match any manager_id, you can do that via a whereIn() clause, combined with the pluck() method:
$query->whereIn('id', $expiringUsers->pluck('manager_id'));
And, your code can be simplified to:
public function scopeManagers(){
return $this->where(function($query){
$query->whereIn('id', $this->ExpiringContractors()->pluck('manager_id'));
})->get();
}
Note: ExpiringContractors should really be expiringContractors; function names are generally camelCase.

Laravel - whereHas with where affect other rows

I have an application where I want to fetch parent records based on children conditionals. Current problem is that I have Students, where they have multiple study fields and study fields belong to one faculty. Pivot table students_study_fields has attribute study_status_id.
What I need is, for example, fetch all students and their study fields which belongs to "prf" faculty AND pivot has study_status_id = 1.
So I write a query like this.
return Student::with(['studyfields' => function ($query1) use ($studyStatusId, $facultyAbbreviation) {
$query1->whereHas('pivot', function ($query2) use ($studyStatusId, $facultyAbbreviation) {
$query2->where('study_status_id', $studyStatusId);
});
$query1->whereHas('studyprogram', function ($query4) use ($facultyAbbreviation) {
$query4->whereHas('faculty', function ($query5) use ($facultyAbbreviation) {
$query5->where('abbreviation', $facultyAbbreviation);
});
});
}])->get();
But this query fetch students witch study_status_id = 2 as well because exists record where this same study field (its code) has relation with student, where study_status_id = 1.
So I don't want to include this studyfield if somewhere exists record with status = 1 in pivot but only if has status = 1 for current row
You need to chain the queries...
return Student::with(['studyfields' => function ($query1) use ($studyStatusId, $facultyAbbreviation) {
$query1->whereHas('pivot', function ($query2) use ($studyStatusId, $facultyAbbreviation) {
$query2->where('study_status_id', $studyStatusId);
})->whereHas('studyprogram', function ($query4) use ($facultyAbbreviation) {
$query4->whereHas('faculty', function ($query5) use ($facultyAbbreviation) {
$query5->where('abbreviation', $facultyAbbreviation);
});
});
}])->get();
Otherwise it will re-start the query1 so you won't get AND kind of query, only get the second part
Side Note: However, I want to warn you that whereHas is a slow query if you have many rows as it goes through each value. I personally prefer grabbing the ids with simple ->where queries and utilise ->whereIn approach.
I found solution for my situation
$students = Student::with(['studyfields' => function ($q) use ($studyStatusId) {
$q->whereHas('pivot')->where('study_status_id', $studyStatusId);
}])
->whereHas('studyfields', function ($q) use ($facultyAbbreviation) {
$q->whereHas('studyprogram', function ($q) use ($facultyAbbreviation) {
$q->where('faculty_abbreviation', $facultyAbbreviation);
});
})
->get();
$students = $students->filter(function ($student) {
return count($student->studyfields) > 0;
})->values();
Query above fetch all students from specific faculty and if studyfields array doesn't contains specific study_status, leave empty array so later I can filter collection from empty arrays assuming that each student belongs to at least one studyfield.

How to order by pivot table data in Laravel's Eloquent ORM

In my Database, I have:
tops Table
posts Table
tops_has_posts Table.
When I retrieve a top on my tops table I also retrieve the posts in relation with the top.
But what if I want to retrieve these posts in a certain order ?
So I add a range field in my pivot table tops_has_posts and I my trying to order by the result using Eloquent but it doesn't work.
I try this :
$top->articles()->whereHas('articles', function($q) {
$q->orderBy('range', 'ASC');
})->get()->toArray();
And this :
$top->articles()->orderBy('range', 'ASC')->get()->toArray();
Both were desperate attempts.
Thank you in advance.
There are 2 ways - one with specifying the table.field, other using Eloquent alias pivot_field if you use withPivot('field'):
// if you use withPivot
public function articles()
{
return $this->belongsToMany('Article', 'tops_has_posts')->withPivot('range');
}
// then: (with not whereHas)
$top = Top::with(['articles' => function ($q) {
$q->orderBy('pivot_range', 'asc');
}])->first(); // or get() or whatever
This will work, because Eloquent aliases all fields provided in withPivot as pivot_field_name.
Now, generic solution:
$top = Top::with(['articles' => function ($q) {
$q->orderBy('tops_has_posts.range', 'asc');
}])->first(); // or get() or whatever
// or:
$top = Top::first();
$articles = $top->articles()->orderBy('tops_has_posts.range', 'asc')->get();
This will order the related query.
Note: Don't make your life hard with naming things this way. posts are not necessarily articles, I would use either one or the other name, unless there is really need for this.
For Laravel 8.17.2+ you can use ::orderByPivot().
https://github.com/laravel/framework/releases/tag/v8.17.2
In Laravel 5.6+ (not sure about older versions) it's convenient to use this:
public function articles()
{
return $this->belongsToMany('Article', 'tops_has_posts')->withPivot('range')->orderBy('tops_has_posts.range');
}
In this case, whenever you will call articles, they will be sorted automaticaly by range property.
In Laravel 5.4 I have the following relation that works fine in Set model which belongsToMany of Job model:
public function jobs()
{
return $this->belongsToMany(Job::class, 'eqtype_jobs')
->withPivot(['created_at','updated_at','id'])
->orderBy('pivot_created_at','desc');
}
The above relation returns all jobs that the specified Set has been joined ordered by the pivot table's (eqtype_jobs) field created_at DESC.
The SQL printout of $set->jobs()->paginate(20) Looks like the following:
select
`jobs`.*, `eqtype_jobs`.`set_id` as `pivot_set_id`,
`eqtype_jobs`.`job_id` as `pivot_job_id`,
`eqtype_jobs`.`created_at` as `pivot_created_at`,
`eqtype_jobs`.`updated_at` as `pivot_updated_at`,
`eqtype_jobs`.`id` as `pivot_id`
from `jobs`
inner join `eqtype_jobs` on `jobs`.`id` = `eqtype_jobs`.`job_id`
where `eqtype_jobs`.`set_id` = 56
order by `pivot_created_at` desc
limit 20
offset 0
in your blade try this:
$top->articles()->orderBy('pivot_range','asc')->get();
If you print out the SQL query of belongsToMany relationship, you will find that the column names of pivot tables are using the pivot_ prefix as a new alias.
For example, created_at, updated_at in pivot table have got pivot_created_at, pivot_updated_at aliases. So the orderBy method should use these aliases instead.
Here is an example of how you can do that.
class User {
...
public function posts(): BelongsToMany {
return $this->belongsToMany(
Post::class,
'post_user',
'user_id',
'post_id')
->withTimestamps()
->latest('pivot_created_at');
}
...
}
You can use orderBy instead of using latest method if you prefer. In the above example, post_user is pivot table, and you can see that the column name for ordering is now pivot_created_at or pivot_updated_at.
you can use this:
public function keywords() {
return $this->morphToMany(\App\Models\Keyword::class, "keywordable")->withPivot('order');
}
public function getKeywordOrderAttribute() {
return $this->keywords()->first()->pivot->order;
}
and append keyword attribiute to model after geting and use sortby
$courses->get()->append('keyword_order')->sortBy('keyword_order');

Categories