Laravel - whereHas last relation is = 'manual' - php

I have a model Lead which 'belongsToMany' Status through lead_has_status table.
What I want is to get all the Leads where the last (current) status of that lead is 'manual'.
I've tried this
$leads = Lead::whereHas('statuses', function ($query) use ($q) {
$query->where('description', 'LIKE', '%' . $q . '%');
})->get();
But since the Lead still has it's old statuses linked to it, it returns every Lead that at some point had Status 'manual'. But I only want the Leads where current status is 'manual'. So I want to do something like
$leads = Lead::whereLast('statuses', function ($query) use ($q) {
$query->where('description', 'LIKE', '%' . $q . '%');
})->get();
Or
$leads = Lead::whereLast('statuses', function ($query) use ($q) {
$query->latest()->first()->where('description', 'LIKE', '%' . $q . '%');
})->get();
(I know this it not the way to do it, it's simply to express what I'm trying to do)
I hope the question makes sense, I am super confused right now.

AFAIK there is no built-in eloquent function that gives you this kind of behaviour. If you would like to solve this with SQL only, you would have to look at the other answer using SQL function MAX in a sub-query and add raw queries in your Laravel code (probably a little bit more efficient then rejecting non-matching).
However you can filter your collection after retrieval and reject the elements that doesn't match your criteria.
Assuming the variable $q is of type string and contains a status e.g. "manual", something like this should do the job (code not tested).
$leads = Lead::whereHas('statuses', function ($query) use ($q) {
$query->where('description', 'LIKE', '%' . $q . '%');
})
->with('statuses')
->get()
->reject(function($element) use($q) {
$lastStatus = $element->statuses[count($element->statuses)-1];
return (strpos($lastStatus->description, $q) === false);
});
If you're column description should contain the exact string "manual" I would do it this way instead:
$leads = Lead::whereHas('statuses', function ($query) {
$query->where('description', 'manual');
})
->with('statuses')
->get()
->reject(function($element) {
$lastStatus = $element->statuses[count($element->statuses)-1];
return $lastStatus->description !== 'manual';
});
The whereHas method will not remove any relations like you need. The whereHas method will only say "select the records that have one or more children that matches this criteria", however all of the children records will be accessible, even the once that doesn't matches your criteria, as long as the parent has one that matches.
What you would like to say is "select the records that have one or more children that matches this criteria (whereHas) THEN remove all parents where the last child record does not match the record I am looking for (reject)"
Edit: Updated code using ->with('statuses') for improved performance.

It looks like you already have a great Laravel answer, but in case you end up going down the native query option, which you can then hydrate, here's a raw sql option... At the least, I hope it might help you visualise what you're trying to achieve.
CREATE TABLE leads (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`info` varchar(100) NULL default NULL,
PRIMARY KEY (`id`)
);
CREATE TABLE statuses (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`leads_id` int(11) null default null,
`description` varchar(100) NULL default NULL,
PRIMARY KEY (`id`)
);
INSERT INTO `leads` (`info`) VALUES ('foo'), ('bar'), ('test');
INSERT INTO `statuses` (`leads_id`, `description`) VALUES
(1, 'manual'),
(1, 'auto'),
(1, 'auto'),
(2, 'auto'),
(2, 'auto'),
(2, 'manual'),
(3, 'manual'),
(3, 'manual'),
(3, 'manual');
SELECT l.*
FROM statuses s
JOIN leads l ON l.id = s.leads_id
WHERE s.id IN (
SELECT MAX(s.id)
FROM leads l
JOIN statuses s ON s.leads_id = l.id
GROUP BY l.id
)
AND s.description LIKE '%manual%'

My solution is to define a new relationship on the Lead table.
class Lead extends Model
{
public function lastStatus()
{
return $this->statuses()->latest()->limit(1);
}
}
This way, the final query can be a bit simpler.
$leads = Lead::whereHas(‘lastStatus’, function ($query) use ($q) {
$query->where(‘description’, ‘LIKE’, $q);
}

Related

Is it possible within Laravel's Query Builder to left join one table with two keys where if one exists, it takes presedence?

I am trying to left join a table in Eloquent Query Builder but the table has two potential foreign keys. I only want to join once but if a potential other column is not null, then it should take presedence.
DB::connection('...')->table('some_table')
->leftJoin('table_that_should_take_presedence as ttstp', 'ttstp.id', '=', 'some_table.ttstp_id')
->leftJoin('some_other_table', function ($otherTable) {
$otherTable->on('some_other_table.id', '=', 'ttstp.some_other_table_id');
//->.... if ttstp.some_other_table_id does not exist then
// some_table.some_other_table_id should be used
});
So far, my solution is to join the table twice and use a coalesce to enable that presedence:
DB::connection('...')->table('some_table')
->selectRaw('coalesce(sot.example, sot2.example, null) as example')
->leftJoin('table_that_should_take_presedence as ttstp', 'ttstp.id', '=', 'some_table.ttstp_id')
->leftJoin('some_other_table as sot', 'sot.id', '=', 'ttstp.some_other_table_id');
->leftJoin('some_other_table as sot2', 'sot2.id', '=', 'some_table.some_other_table_id');
This doesn't seem slick, I feel the optimal solution is joining once. Any help appreciated. Thanks in advance.
As I understand you correct, you want this:
$q = DB::connection('...')
->table('some_table')
->selectRaw('sot.example')
->leftJoin('table_that_should_take_presedence as ttstp', 'ttstp.id', '=', 'some_table.ttstp_id')
// Try to join by ttstp.some_other_table_id column, if it's null then use some_table.some_other_table_id column
->leftJoin('some_other_table as sot', 'sot.id', '=', DB::raw('COALESCE(ttstp.some_other_table_id, some_table.some_other_table_id)'));
dump($q->toSql());

Eloquent relationship where not in sub-query

I have got a relationship where I select all row's based on the category, however I need to exclude some of these if they are within a sub query.
/** #var \Illuminate\Database\Query\Builder $images */
$images = $vehicle->images()
->whereIn('image_category', $website->image_categories)
->orderBy('seq', 'ASC');
$images->whereNotIn('id', static function ($q) {
return $q->select('id')
->whereIn('image_category', [0, 99])
->groupBy('seq')
->having(DB::raw('count(`seq`)'), '>', 1);
});
dd($images->toSql(), $images->getBindings());
So above is my code, nearly works as I want it, however it seems that the $q variable doesn't have the table name within the query, below is the query outputted:
select
*
from
`vehicle_images`
where
`vehicle_images`.`vehicle_id` = ?
and `vehicle_images`.`vehicle_id` is not null
and `image_category` in (?, ?)
and `id` not in (
select
`id`
where
`image_category` in (?, ?)
group by
`seq`
having
count(`seq`) > ?
)
order by
`seq` asc
This is the relationship:
public function images()
{
return $this->hasMany(VehicleImage::class);
}
You can specify what table you want to use.
$images->whereNotIn('id', static function ($q) {
return $q->select('id')->from('{CORRECT_TABLE_NAME_HERE}')
->whereIn('image_category', [0, 99])
->groupBy('seq')
->having(DB::raw('count(`seq`)'), '>', 1);
});
I don't know what exactly the table name should be, hence the placeholder.

Eloquent add optional condition to hasMany where foreign key is null

How do you add an optional/OR condition to a eloquent relationship?
E.g I want all the users comments OR where the foreign key (user_id) is NULL.
select * from `comments` where (`user_id` is null OR `comments`.`user_id` in (1, 2, 3)) AND `status` = 1
In the User relationship added orWhereNull
public function comments() {
return $this->hasMany(Comments::class)->orWhereNull('user_id');
}
But Laravel it's running:
select * from `comments` where `user_id` is null and `comments`.`user_id` in (1, 2, 3)
Surprised this hasn't been asked before only thing I found similar was this:
https://laracasts.com/discuss/channels/eloquent/eloquent-orwherenull-when-where-has-no-results
I tried this but it needs the model not the query builder.
return $this->where(function ($query){
return $query::hasMany(Comment::class)->orWhereNull('user_id');
});
I'm using eager loading to fetch the comments for a list of users.
$users = User::with('comments')->where('active', 1)->paginate(10);
It doesn't work because the "orWhere" is sent to the underlying query builder immediately but the foreign key constraint is added when the query is run. I couldn't work out a nice way to sort that but this workaround is fine for my use case where I'm only selecting one row at a time with this relation (granted I should probably use replace it with a getter...):
(Excuse any mistakes, changing model names for clarity):
class Customer
{
public function selectedOrCurrentWeek(): HasOne
{
return $this->hasOne(Week::class, 'id', 'selected_week_id')
->withDefault(function (Week $instance, Customer $parent) {
return $instance->newQuery()
->whereRaw('CURRENT_TIMESTAMP between start_date and end_date')
->where('customer_id', $parent->id)
->first();
});
}
Query log when fetching a customer by ID :-
select * from `customers` where
`customers`.`id` = ?
and `customers`.`deleted_at` is null
limit 1;
select * from `weeks` where
CURRENT_TIMESTAMP between start_date and end_date
and `customer_id` = ?
and `weeks`.`deleted_at` is null
limit 1;
but it will run the second query once per row
You can optimize this further to your need, just giving an idea on query
$users = User::with('comments', function($query){
$query->where('user_id', '=', null)->where('status', '1');
})->paginate(10);

laravel eloquent ->whereHas - write your own exists( subquery )

Laravel Eloquent ->whereHas() uses anexists() subquery - https://dev.mysql.com/doc/refman/8.0/en/exists-and-not-exists-subqueries.html - in order to return your results.
I would like to write my own subquery, but I do not know how to tell Eloquent to ->where it.
If I do:
$query->where( DB::raw(' exists( subquery ) ')
Laravel instead writes the subquery as:
where exists( subquery ) is null
So I'm just wondering what $query->method() could be used to add an exists() subquery to the 'where' statements. The subquery would be just the same kind that laravel generates, but written out:
... and exists ( select * from `tbl` inner join `assets` on `custom_assets`.`id` = `tbl`.`asset_id` where `assets`.`deleted_at` is null and `users`.`id` = `assets`.`client_id` and `field_id` = ? and (`value` = ? and `assets`.`deleted_at` is null )
Use whereRaw():
$query->whereRaw('exists( subquery )')
Read WhereHas Description Here
You can find this code example there. You can also add a closure for you custom query in whereHas.
// Retrieve all posts with at least one comment containing words like foo%
$posts = App\Post::whereHas('comments', function ($query) {
$query->where('content', 'like', 'foo%');
})->get();

Order By before Group By using Eloquent (Laravel)

I have a "messages" table with the following columns
CREATE TABLE `messages` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`fromId` int(11) NOT NULL,
`toId` int(11) NOT NULL,
`message` text NOT NULL,
`status` int(11) NOT NULL,
`device` varchar(100) NOT NULL,
`createdAt` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=57 DEFAULT CHARSET=latin1;
I'm trying to get all messages where 'toId' = $id and grouping by fromId. The problem is that the "message" shown on the results is the first ones, not the latest ones. I tried ordering by createdAt but it's not working.
How can I order by "createdAt" prior to querying and grouping the results? I want to do this in the laravel way using Eloquent.
My query:
$chats = Message::with('sender','recipient')
->where('toId',$id)
->orderBy('createdAt')
->groupBy('fromId')
->paginate(10)
I just needed to do something similar with a messages model. What worked for me was applying the unique method on the returned eloquent collection.
Model::where('toId', $id)
->orderBy('createdAt', 'desc')
->get()
->unique('fromId');
The query will return all messages ordered by createdAt and the unique method will reduce it down to one message for each fromId. This is obviously not as performant as using the database directly, but in my case I have further restrictions on the query.
Also, there are many more useful methods for working with these collections: https://laravel.com/docs/5.2/collections#available-methods
I found a way to do that! Basically, creating a subquery and running it before, so that results are ordered as expected and grouped after.
Here is the code:
$sub = Message::orderBy('createdAt','DESC');
$chats = DB::table(DB::raw("({$sub->toSql()}) as sub"))
->where('toId',$id)
->groupBy('fromId')
->get();
Try this query :
$chats = Message::with('sender','recipient')
->where('toId',$id)
->whereRaw('id IN (select MAX(id) FROM messages GROUP BY fromId)')
->orderBy('createdAt','desc')
->paginate(10)
It should be something along this:
Message::whereToId($id)->groupBy('fromId')->latest('createdAt')->first();
Update
After seeing the query that you've added, you probably just need to add a direction to the orderBy function, like this:
$chats = Message::with('sender','recipient')
->select(DB::raw('*, max(createdAt) as createdAt'))
->where('toId',$id)
->orderBy('createdAt', 'desc')
->groupBy('fromId')
->paginate(10)
This work for me:
(you can remove the order by createdAt in case createAt increase along with id)
DB::select(DB::raw('select * from (select * from messages group by fromId desc) m order by m.createdAt'));
$data = MCCAddMilkEntry::where('coming_from','=','society')
->whereRaw('id = (select max(`id`) from mcc_add_milk_entry)')->get();
#Sergio your answer is good but what if someone wanted to select a related model along with....?
Answer : I have tested every single function but I didn't fetch the correct data.
unique() gives data with associated array.
groupBy() gives better but picking first message.
Solution for Latest inbox messages. My case:
$data = ChatMessage::where('incoming_msg_id', auth()->user()->id)->orWhere('outgoing_msg_id', auth()->user()->id)->latest('msg_id')->with('messageable')->get();
$inbox = [];
$itemIds = [];
foreach ($data as $key => $value) {
if (!in_array($value['messageable_id'].$value['msg_for'], $itemIds)) {
array_push($inbox,$value);
array_push($itemIds,$value['messageable_id'].$value['msg_for']);
}
}
return $this->sendResponse($inbox,'Inbox');
Your case:
$chats = Message::where('toId',$id)->orWhere('fromId', $id)->latest('id')->with('sender','recipient')->get();
$inbox = [];
$fromIds = [];
foreach ($chats as $key => $value) {
if (!in_array($value['fromId'], $fromIds)) {
array_push($inbox,$value);
array_push($fromIds,$value['fromId']);
}
}
return $this->sendResponse($inbox,'Inbox');

Categories