Home » Php » php – Laravel – whereHas last relation is = 'manual'

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

Posted by: admin February 25, 2020 Leave a comment

Questions:

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.

How to&Answers:

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.

Answer:

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%'

Answer:

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