Chain whereHas to traverse through a self-referencing model - laravel

Using Laravel 5.4....
Imagine I have the following models:
App\Location
This has a self referencing hierarchy, and has the following data
UK
|---North West
| |----Liverpool
| |----Manchester
|---North East
|----Newcastle
|----Sunderland
In this model I have a self relation
public function parent()
{
return $this->belongsTo('App\Location', 'location_id');
}
and a recursive relation...
public function parentRecursive()
{
return $this->parent()->with('parentRecursive');
}
App\Shop
The shop model has a 'location' relation.
public function location()
{
return $this->belongsTo('App\Location', 'location_id');
}
What I want to do is to get all of the shops within a category. So if I have a shop called "ACME" that is related to "Liverpool", I can easily get it by sending the ID for "Liverpool" (as $value) in the following condition....
->whereHas('location', function($q) use ($value) {
$q->where('id', $value);
})
But technically, this shop is also in "North West" and in "UK".
So if I send the ID for the UK Location to that query, it will not return the ACME shop as it is not directly related to North West or UK ID.
I can get it working by sending the UK id ($value) to this...
$this->builder->whereHas('location', function($q) use ($value) {
$q->where('id', $value);
})->orWhereHas('location.parent', function($q) use ($value) {
$q->where('id', $value);
})->orWhereHas('location.parent.parent', function($q) use ($value) {
$q->where('id', $value);
});
So is there a better way to write the above which is ugly and would only work for a finite number of 'jumps' in the relation tree? I need it to traverse all of the locations until it reaches the top of the tree.

I encountered the same problem. My solution is not that elegant, but it works without having to use a nested set model:
First, (if you haven't done yet) you may need to create an inverse of your existing recursive relation:
public function children() {
return $this->hasMany('App\Location', 'location_id');
}
public function childrenRecursive() {
return $this->children()->with('childrenRecursive');
}
Next, you need to create a recursive function to get the max depth
public function getDepth($location) {
$depth = 0;
foreach ($location->children as $child) {
$d = $this->getDepth($child);
if ($d > $depth) {
$depth = $d;
}
}
return 1 + $depth;
}
Then, you can use the depth to chain your query with multiple orWhereHas clause
$whereHas = 'location';
$query = Shop::whereHas($whereHas, function ($q) use ($value) {
$q->where('id', $value);
});
for ($d = 1; $depth > $d; $d++) {
$whereHas = $whereHas . '.parent';
$query->orWhereHas($whereHas, function ($q) use ($value) {
$q->where('id', $value);
});
}

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 - Filter a collection

I am retrieving a bunch of records from the database to display:
$students = $bursary_administrator->students()/*->whereIn('status',[1,4,6,7])*/
->whereHas('bursaries.statuses', function($query) use ($request) {
$query->whereIn('status',[1,4,6,7])
->whereYear('status_start','<=',$request->year)
->where(function ($query) use ($request){
$query->whereYear('status_end','>=',$request->year)
->orWhereNull('status_end');
})
->where(function ($query) use ($request){
$query->whereYear('registration_date','<=',$request->year)
->orWhereNull('registration_date');
});
})
->with('bursaries','bursaries.statuses','bursaries.enrolments','bursaries.enrolments.courses')
->orderBy('student_name')->orderBy('student_middle_names')->orderBy('student_surname')->get();
Im displaying all these records in a table so need to fetch all of them.
I also however, need to get some count()'s based on this data. Now, I know I can perform individual queries for each count, but since I've already fetched all the data, I think it will be more optimised to just count the filtered data?
However, I am not winning. Both attempts listed below yield 0 as the output:
eg: using "where":
$summary['students_transfer'] = $students->where('status', '7')->count();
or, using "filter":
$summary['students_postgraduate'] = $students->filter(function ($student) {
return $student->qualification == 'Postgraduate' || $student->qualification == 'Postgraduate in Training';
})->count();
Not sure what I'm doing wrong?
You can utilize scopes in combination with withCount():
class Student extends Model
{
public function scopeTransfer($query)
{
return $query->where('status', '7');
}
public function scopePostgraduate($query)
{
return $query->where('qualification', 'Postgraduate')
->orWhere('qualification', 'Postgraduate in Training');
}
}
class Bursary_Administrator extends Model
{
public function students()
{
return $this->hasMany(Student::class);
}
public function students_transfer()
{
return $this->hasMany(Student::class)->transfer();
}
public function students_postgraduate()
{
return $this->hasMany(Student::class)->postgraduate();
}
}
$students = $bursary_administrator->students()/*->whereIn('status',[1,4,6,7])*/
->whereHas('bursaries.statuses', function($query) use ($request) {
$query->whereIn('status',[1,4,6,7])
->whereYear('status_start','<=',$request->year)
->where(function ($query) use ($request){
$query->whereYear('status_end','>=',$request->year)
->orWhereNull('status_end');
})
->where(function ($query) use ($request){
$query->whereYear('registration_date','<=',$request->year)
->orWhereNull('registration_date');
});
})
->with('bursaries','bursaries.statuses','bursaries.enrolments','bursaries.enrolments.courses')
->withCount('students_transfer', 'students_postgraduate')
->orderBy('student_name')->orderBy('student_middle_names')->orderBy('student_surname')->get();
Reason it wasnt working was because I was thinking of it as a database result instead of parent/child relationship.
ie, instead of:
$summary['students_postgraduate'] = $students->filter(function ($student) {
return $student->qualification == 'Postgraduate' || $student->qualification == 'Postgraduate in Training';
})->count();
I should of been doing:
$summary['students_postgraduate'] = $students->filter(function ($student) {
return $student->bursaries[0]->qualification == 'Postgraduate' || $student->bursaries[0]->qualification == 'Postgraduate in Training';
})->count();

How to use whereHas method in Laravel scout,

I have been trying to search in both the name column and that's relationship. I have coded as below;
///
$posts = ContentForSearch::with("content_type","content_sub_type")->exclude($exclude)->user($user_id_filter)
->where('name', 'LIKE', "%{$search}%")
->orWhereHas('tags', function ($q) use ($search) {
$q->where('tag', 'LIKE', "%{$search}%");
})
->orderBy($sort, $order)
->paginate($limit);
I have set a searchable Model. I want to change with search method as below;
$posts = ContentForSearch::search($search)->orWhere(function ($query) use ($search) {
$query->whereHas('tags', function ($q) use ($search) {
$q->where('tag', 'LIKE', "%{$search}%");
});
})
->orderBy($sort, $order)
->paginate($limit)->load("content_type","content_sub_type");
Why isnt the code working ? How can i fix it ?
Model is:
class ContentForSearch extends Model{
use Searchable;
protected $table = 'content';
public function searchableAs()
{
return 'contents_index';
}
public function toSearchableArray()
{
$array = $this->toArray();
// Customize array...
return ["name"=> null];
}
public function content_type()
{
return $this->belongsTo(ContentType::class, 'content_type_id');
}
public function tags()
{
return $this->belongsToMany(ContentTag::class, 'content_content_tags', 'content_id', 'content_tags_id')->withTimestamps();
}
// * * *
The way fulltext search works with algolia and elastic, it seems weird that you want to preserve whereHas() relationship method. In my opinion it is an either or case, either you want sql relation search or you want full text, if you want full text, i would map all the tags at index building instead. So something like this would work.
public function toSearchableArray()
{
$customArray = $this->toArray();
$index = 1;
$this->tags->each(function(Tag $tag) use (&$customArray, &index) {
$customArray['tag' . $index++] = $tag->tag;
});
return $customArray;
}
Which will result in an object similar to this being indexed.
{
'name' => 'x',
'tag1' => 'comment',
'tag2' => 'post',
}
Now searching would provide a similar functionality.
ContentForSearch::search($search);

Laravel eloquent search functionality

I'm currently creating a search functionality on my index blade file.
I have different users that have different stores per area.
my User model:
function area() {
return $this->hasOne('App\Area');
}
function role() {
return $this->belongsTo('App\Role');
}
function reports() {
return $this->hasMany('App\Report');
}
function stores() {
return $this->hasManyThrough('App\Store', 'App\Area');
}
}
Area model:
function reports() {
return $this->hasMany('App\Report');
}
function stores() {
return $this->hasMany('App\Store');
}
function user() {
return $this->belongsTo('App\User');
}
Store model:
function district() {
return $this->belongsTo('App\District');
}
function cluster() {
return $this->belongsTo('App\Cluster');
}
function city() {
return $this->belongsTo('App\City');
}
function area() {
return $this->belongsTo('App\Area');
}
I have managed to make the functionality work on admin account (where in you can see all stores) by:
function index(Request $request) {
if($request->has('search')) {
$stores = Store::whereHas('city', function($query) use($request) {
$query->where('name', 'like', '%' . $request->search . '%');
})->orWhereHas('cluster', function($query) use($request) {
$query->where('name', 'like', '%' .$request->search. '%');
})->orWhere('name', 'like', '%' .$request->search. '%')
->orWhere('store_no', 'like', '%' .$request->search. '%')->paginate(10);
} else {
$stores = Store::orderBy('created_at', 'desc')->paginate(10);
}
return view('/stores.store_list', compact('stores'));
what I want to do on user accounts is:
function index(Request $request) {
if($request->has('search')) {
$id = Auth::user()->id;
$query = $request->search;
$user = User::find($id)->stores()->where('name', 'like', '%' . $query . '%')->paginate(10);
} else {
$id = Auth::user()->id;
$user = User::find($id)->stores()->orderBy('created_at', 'desc')->paginate(10);
}
return view('/stores.store_list', compact('user'));
}
I'm having an error when searching:
SQLSTATE[23000]: Integrity constraint violation: 1052 Column 'name' in where clause is ambiguous (SQL: select count(*) as aggregate from stores inner join areas on areas.id = stores.area_id where areas.user_id = 6 and name like %houston% and stores.deleted_at is null)
can anyone point me on the right direction and let me know what i'm missing here? Thank you!
When you have error such is
Column 'some_column_name_here' in where clause is ambiguous...
it means that there is complex query associated with more tables of which at least two those have same table name in their structure and that table name is used in complex query (in some ON, WHERE...) in this case WHERE as error states.
You always can use table_name.column_name {table name dot column name} syntax to point syntax at exact table meant (ambiguous).

tojson() does not return all related models

I am working on laravel 5.5 project
i have four tables as following:-
1- Subject model (has following relations)
public function types()
{
return $this->belongsToMany('App\Types');
}
public function areas()
{
return $this->belongsToMany('App\Area');
}
public function articles()
{
return $this->hasMany('App\Article');
}
2- Area and Types has many to many relation with Subject model
public function subjects()
{
return $this->belongsToMany('App\Subjects');
}
3- Article has 1 to many relation with Subject
public function subjects()
{
return $this->belongsTo('App\Subjects');
}
The below controller will search for subject by keyword, select type or area and return value as json using toJson() and return results to view:-
public function search(Request $request)
{
//start search//
$get_Subjects = new Subject();
$get_Subjects = $get_Subjects ->newQuery();
if($request->term != '')
{
$get_Subjects->with('articles')->whereHas('articles', function ($query) use ($request){
$query->where('title', 'LIKE', '% '.$request->term.' %')
->orwhere('abstract', 'LIKE', '% '.$request->term.' %')
->orwhere('fullCitation', 'LIKE', '% '.$request->term.' %');
})
->where(function($query) use ($request){
$query->where('name', 'LIKE', '%'.$request->term.'%');
});
}if($request->area != '28')
{
// search for the selected area
$get_Subjects->with('areas')->whereHas('areas', function($query) use ($request){
$query->where('area_id', $request->area);
});
}
if($request->type!= '36')
{
// search for the selected types
$get_Subjects->with('types')->whereHas('types', function($query) use ($request){
$query->where('type_id', $request->type);
});
}
$subjects = $get_Subjects->tojson();
return View::make('public.search', compact('subjects'));
}
the issue is when i search by keyword the returned json only include related articles, and if i search by area i only get related areas()
while if i did not use json and access variable from blade i can access all related models even though it is the same query
i tried to use load() but it did not work
any help will be valuable
============Update===========
i tried using load() as follow
$subjects= $get_subjects->get();
$subjects= $subjects->load('articles', 'areas', 'types')->tojson();
when search by keyword, the json does have related relations
but they are empty
when search by area or type, the json does have related relation
which mean if i search in the subject table only which is the 1st case i wont get related model with it
please need your help
Have you tried adding the ->get() method before the ->toJson() method?
$subjects = $get_Subjects->get()->toJson();
get() will run the query and return models. toJson() should then work as you expect.
Update
I would write your code like this to start with. (I haven't tested this).
It seems like you are using the whereHas the wrong way. The subject table should have the area_id and type_id fields, so whereHas isn't needed... this might not be the case, but it seems like it should be based on the info you provided.
public function search(Request $request)
{
$get_Subjects = new Subject();
$get_Subjects = $get_Subjects ->newQuery();
$with = [];
if($request->term != '') {
$with[] = 'articles';
$subjects = $get_Subjects->whereHas('articles', function ($query) use ($request){
$query->where('title', 'LIKE', '% '.$request->term.' %')
->orwhere('abstract', 'LIKE', '% '.$request->term.' %')
->orwhere('fullCitation', 'LIKE', '% '.$request->term.' %');
})
->where('name', 'LIKE', '%'.$request->term.'%');
}
if($request->area != '28') {
$with[] = 'areas';
$get_Subjects->where('area_id', $request->area);
}
if($request->type!= '36') {
$with[] = 'types';
$get_Subjects->where('type_id', $request->type);
}
$subjects = $get_Subjects->with($with)->get()->toJson();
return View::make('public.search', compact('subjects'));
}

Resources