laravel collection nested "where" - php

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

Related

Laravel - latest() chained with where() behavior

So I have a many-to-many table with created_at and the foreign keys student_id and status_id.
For each student, I want to retrieve an entry only if the latest one has status_id = 1.
I tried it like this: (it's a chain of queries, in this case $query would be Student::where(something else))
$query->whereHas('statusuri', function($query) use ($statusuri) {
$query->latest('status_student.created_at')->where('status_id', 1);
});
(statusuri is the many-to-many relationship)
but I get a different result from what I need.
It does the where clause first and then latest(). Basically, it retrieves the last entry which matches the where.
I want to search for each student, the latest entry and if the where clause matches it, get that entry. If not, don't return anything.
Is that possible with Eloquent?
Thanks.
In your case, you can use the where and latest methods together in a subquery, like this:
$query->whereHas('statusuri', function($query) use ($statusuri) {
$query->where(function($query) {
$query->where('status_id', 1)
->latest('status_student.created_at');
});
});
Have you considered a "has one of many" relationship : https://laravel.com/docs/9.x/eloquent-relationships#advanced-has-one-of-many-relationships
You can make a relationship in your Student model:
public function latestStatus()
{
return $this->hasOne(StatusStudent::class)->ofMany([
'created_at' => 'max',
'id' => 'max',
], function ($query) {
$query->where('status_id', 1);
});
}
Edit: works only for Laravel 8+

How to check relation data of a collection before sending it to view

I have a Controller method like this:
public function awaiting()
{
$producers = Producer::where('producer_process',4)->get();
$producers_list = [];
foreach($producers as $producer){
if($producer->brand->brand_rejected == 0){
array_push($producers_list, $producer);
}
}
return view('admin.brands.awaiting', compact('producers_list'));
}
So basically there's One To One relationship between Producer model & Brand model.
In order to get the collection of brands table records that has producer_process of 4 and ALSO the brand_rejected field of related brands table record must be set to 0, I added an array_push and check the condition.
Now this works fine and properly shows the correct data but I wanted to know, what is the shorthand method of doing this with Eloquent relationships?
I mean is there any concise and useful method written in Eloquent relationships that can do this without using array_push or another foreach loop?
You can use whereHas to constrain the result set based on the existence of a relationship. Here we are saying we only want producers that have the field 'produce_process' set to 4 and have a brand with a field of 'brand_rejected' set to 0:
$producers = Producer::where('producer_process', 4)
->whereHas('brand', function ($q) { $q->where('brand_rejected', 0); })
->get();
If you want these producers to have their brand relationship loaded to use you should eager load that. Before the get call you can tell it to load the relationship:
$producers = Producer::where(...)->whereHas(...)->with('brand')->get();
Laravel 5.8 Docs - Eloquent - Relationships - Querying Relationship Existence whereHas
Laravel 5.8 Docs - Eloquent - Relationships - Eager Loading with
You can try this:
public function awaiting()
{
$producers = Producer::where('producer_process',4)
->with('brand', function($q) {
$q->where('brand_rejected', 0);
})->get();
// dd($producers);
dd($producers->pluck(brand));
Sure you can use the method with() also with the where() clause to can apply some conditions to the relationship
Example
$yourQuery->with('brand', function($query){
$query->where('brand_rejected', 0);
});
check this for more info
https://laravel.com/docs/9.x/eloquent-relationships#constraining-eager-loads
I hope it's helpful

laravel filter collection based on relation

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.

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

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