Sort parents regarding child attribut using Eloquent - laravel

I have a model 'job' linked with the model 'job_translation' like that :
/**
* Get the translations for the job.
*/
public function translations()
{
return $this->hasMany('App\Models\JobTranslation');
}
I want to build a dynamic api route which can accept several parameters to filter ou sorter a query on the jobs. To do that I have this kind of controller (extract) :
public function index(Request $request)
{
$queryJob = Job::query();
// filter on the job short_name
if ($request->has('short_name')) {
$queryJob->where('short_name', 'ilike', '%'.$request->short_name.'%');
}
// filter on the translation
if ($request->has('translation')) {
$queryJob->whereHas('translations', function ($query) use ($request) {
$queryJob->where('internal_translation', 'ilike', '%'.$request->translation.'%');
});
}
// sort by the external translation
if ($request->has('order.external_translation')) {
// var_dump($request->input('order.external_translation'));
// var_dump($request->server('HTTP_ACCEPT_LANGUAGE'));
$queryJob->whereHas('translations', function ($query) use ($request) {
$query->where('language_id', $request->server('HTTP_ACCEPT_LANGUAGE'));
$query->orderBy('external_translation', 'asc');
});
/*
$queryJob->with(['translations' => function ($query) use ($request) {
$query->where('language_id', $request->server('HTTP_ACCEPT_LANGUAGE'));
$query->orderBy('external_translation', 'asc');
}]);
*/
}
// security for the qty of items per page
$qtyItemsPerPage = 15;
if ($request->has('qtyItemsPerPage')) {
if (is_numeric($request->qtyItemsPerPage) && $request->qtyItemsPerPage <= 50) {
$qtyItemsPerPage = $request->qtyItemsPerPage;
}
}
$jobs = $queryJob->paginate($qtyItemsPerPage);
return JobResource::collection($jobs);
My problem is for the sort condition (the others are OK). When I use this kind of url:
jobs?order[external_translation]=asc
I tried a lot of things without success (commented in the code), the result is never sorted like I want. I think my problem is in the relation between the two models.
So how to sort the parents regarding an attribute of their children, using Eloquent?

You cannot sort nested relationships with the query builder alone. Use the Collection's sortBy method instead.

Related

Eloquent query : Retrieve the list of offices where the user possess all the desks, not just one (nested whereHas)

I want to retrieve all the offices ( with the desks eager loaded) but I only want offices where the user possess all the desks in the office
I have the following models and relationships between them :
I came up with the following query which seems to almost work :
<?php
Office::query()
->whereHas('desks', function ($query) {
$query->whereHas('possessedDesks', function ($query) {
$query->where('user_id', auth()->id);
});
})
->with(['desks'])
->get();
The current query seems to return a result where if a user own a single desk in the office then the office is returned in the query. Am I missing something ? Is there a way to be more strict in the whereHas to have some kind of and instead of a or
Thanks in advance for your help ;)
Edit :
Thanks to Tim Lewis's comment I tried this with not more result :
<?php
Office::query()
->withCount('desks')
->whereHas('desks', function ($query) {
$query
->whereHas('possessedDesks', function ($query) {
$query->where('user_id', auth()->id);
})
->has('possessedDesks', '=', 'desks_count');
})
->with(['desks'])
->get();
Edit 2 :
I managed to get exactly what I need, outside of an Eloquent query. The problem is still persist since I need it to be in an Eloquent query because I need this for a query string request (Search engine).
<?php
$offices = Office::query()
->with(['desks'])
->get();
$possessedDeskIds = auth()->user->with('possessedDesks.desk')->possessedDesks()->get()->pluck('desk.id');
$fullyOwnedOffices = [];
foreach($offices as $office) {
$officeDeskIds = $office->desks()->pluck('id');
$atLeastOneDeskIsNotPossessed = false;
foreach($officeDeskIds as $officeDesk) {
if ($possessedDeskIds->doesntContain($officeDesk)) {
$atLeastOneAromaIsNotPossessed = true;
break;
}
}
if (!$atLeastOneDeskIsNotPossessed) {
$fullyOwnedOffices[] = $office;
}
}
Edit 3 :
Ok, With the previous edit and the need to have some kind of one line query (for the query string of a search engine) I simplified the request since the nested whereHas where hard to make sense of.
It's not the prettiest way to do it, It add more query for the process, but with the code from the Edit2 I can generate an array of Ids of the Office where all the Desk are possessed by the user. With that I can just say that when this option is required in the search engine, I just select the ones my algorithm above gave me and no more logic in the query.
If some genius manage to find a way to optimize this query to add the logic back inside of it, I'll take it but for now it works as expected.
Thanks Tim for your help
<?php
class SearchEngineController extends Controller
{
public function index(Request $request) {
$officesWithAllDesksPossessed = collect([]);
if ($request->has('with_possessed_desks') && $request->input('with_possessed_desks')) {
$publicOffices = Office::query()
->isPublic()
->with(['desks'])
->get();
$possessedDeskIds = currentUser()
->possessedDesks()
->with('desk')
->get()
->pluck('desk.id');
foreach($publicOffices as $office) {
$publicOfficesDeskIds = $office->desks()->pluck('id');
$atLeastOneDeskIsNotPossessed = false;
foreach($publicOfficesDeskIds as $officeDesk) {
if ($possessedDeskIds->doesntContain($officeDesk)) {
$atLeastOneDeskIsNotPossessed = true;
break;
}
}
if (!$atLeastOneDeskIsNotPossessed) {
$officesWithAllDesksPossessed->push($office);
}
}
$officesWithAllDesksPossessed = $officesWithAllDesksPossessed->pluck('id');
}
return Inertia::render('Discover', [
'offices'=> OfficeResource::collection(
Office::query()
->isPublic()
->with(['desks'])
->when($request->input('search'), function ($query, $search) {
$query->where('name', 'like', "%{$search}%");
})
->when($request->input('with_possessed_desks'), function ($query, $active) use($officesWithAllDesksPossessed) {
if ($active === 'true') {
$query->whereIn('id', $officesWithAllDesksPossessed);
}
})
->paginate(10)
->withQueryString()
),
'filters' => $request->only(['search', 'with_possessed_desks']),
]);
}
}

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

Filtering Products according to price filter (from to)

I'm trying to filter my products according to price, using a form in the view, I'm passing from and to as two separate filters and stuck from there.
here's the code in my Product Model:
public function scopeFilter($query, array $filters)
{
$query->when(
$filters['from'] ?? false,
function ($query,$request) {
$query->whereBetween('price', [$request->from,$request->to]);
}
);
}
obviously this isn't working and when I dd the request it's giving me the "from" value only.
any ideas??
this is my product controller:
public function index()
{
$categories = Category::get();
$products = Product::latest()->filter(request(['category','from','to']))->simplePaginate(5);
return view('welcome',compact('products','categories',));
}
at the moment it's giving me an error of "Trying to get property 'to' of non-object"
You are reading the request wrong, Your scope filter function should be like:
public function scopeFilter($query, array $filters)
{
$query->when(
($filters['from'] && $filters['to']) ?? false,
function ($query) use ($filters){
$query->whereBetween('price', [$filters['from'],$filters['to']]);
}
);
}
At the minute, you're just passing the from price to the closure.
When you use when(), the first argument is passed as the second param of the closure (in this case the from price...false won't trigger the closure).
You can either add a use statement to the closure:
public function scopeFilter($query, array $filters)
{
$query->when(
$filters['from'] ?? false,
function ($query) use ($filters) {
$query->whereBetween('price', [$filters['from'], $filters['to']]);
}
);
}
or, if you're using php >= 7.4, you can use an arrow function instead:
public function scopeFilter($query, array $filters)
{
$query->when(
$filters['from'] ?? false,
fn ($query) => $query->whereBetween('price', [$filters['from'], $filters['to']]);
);
}
Furthermore, it would probably make more sense to remove the when entirely as you should really be validating the values in your controller.

Where clause in polymorphic relationship

I have the tables
threads
- id
replies
- id
- repliable_id
- repliable_type
I want to add another column to the Thread, which is the id of the most recent reply.
Thread::where('id',1)->withRecentReply()->get()
And the following query scope
public function scopeWithRecentReply() {
return $query->addSelect([
'recent_reply_id' => Reply::select('id')
->whereHasMorph('repliable', ['App\Thread'], function ($q) {
$q->where('repliable_id', '=', 'threads.id');
})->latest('created_at')
->take(1),
]);
}
I have also tried
public function scopeWithRecentReply() {
return $query->addSelect([
'recent_reply_id' => Reply::select('id')
->where('repliable_id', '=', 'threads.id')
->latest('created_at')
->take(1),
]);
}
But in both cases the
recent_reply_id => null
If instead of threads.id i enter an integer, it works and the recent_reply_id is not null
For example
public function scopeWithRecentReply() {
return $query->addSelect([
'recent_reply_id' => Reply::select('id')
->whereHasMorph('repliable', ['App\Thread'], function ($q) {
$q->where('repliable_id', '=', 1);
})->latest('created_at')
->take(1),
]);
}
My question is
Is there a way to be able to fetch the recent_reply_id using the respective threads.id ?
I would suggest using appends instead of query scopes so in your Thread model we will add
protected $appends = ['recent_reply_id'];
.....
public function replies () {
// you need to add here your relation with replies table
}
public function getRecentReplyIdAttribute () {
return $this->replies()->latest('id')->first()->id;
}
now wherever you query the threads table you will have access to recent_reply_id like
$thread = Thread::where('id',1)->withRecentReply()->get();
$thread->recent_reply_id;

Laravel, sort result on field from relation table?

I have a list with gamers and another table with game stats.
My list code is:
$gamers = Gamer::with(['lastGameStat' => function($query) {
$query->orderBy('total_points', 'DESC');
}])->paginate(20);
relation:
public function lastGameStat() {
return $this->hasOne(GameStat::class, 'gamer_id', 'id')->orderBy('created_at', 'DESC');
}
in relation table I have field: total_points and with this code I thought it's possible to sort list of gamers by total_points $query->orderBy('total_points', 'DESC');
It doesn't work, can somebody give me an advice here how can I sort the result on a field from relation table?
I guess you'll need either another relation or custom scopes to fetch various game stats of a gamer.
Second relation
Gamer.php (your model)
class Gamer
{
public function bestGameStat()
{
return $this
->hasOne(GameStat::class)
->orderBy('total_points', 'DESC');
}
}
Custom scopes
Gamer.php
class Gamer
{
public function gameStat()
{
return $this->hasOne(GameStat::class);
}
}
GameStat.php
use Illuminate\Database\Eloquent\Builder;
class GameStat
{
public function scopeBest(Builder $query)
{
return $query->orderBy('total_points', 'DESC');
}
}
In your controller:
$gamersWithTheirLatestGameStatistic = Gamer::with(['gameStat' => function($query) {
$query->latest();
}])->paginate(20);
$gamersWithTheirBestGameStatistic = Gamer::with(['gameStat' => function($query) {
$query->best();
}])->paginate(20);
Be aware as this is untested code and might not work.

Resources