1

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 :

Database Modelisation

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']),
        ]);
    }
}
dib258
  • 705
  • 8
  • 25
  • *"where if a user own a single desk"* - Indeed; that is how `whereHas()` works If you want to enforce _all_ of them, a separate `has('desks', /* count of all desks in office */)` can be added, so your query will only return if both the `whereHas()` and `has()` clauses pass. I'm not too sure of the syntax you'd use here to calculate that count; see if you can adapt your code to that – Tim Lewis Sep 28 '22 at 19:58
  • 1
    Thanks, it seems to be exactly what I need but I am also a little bit foggy on how to achieve this! I tried with a `->withCount('desks')` and `->has('desks.possessedDesks', '>=', 'desks_count')` but it doesn't seems to change anything with the query. I'm really not sure if this is a the right place. ( I edited the posts to reflect how I tried it) – dib258 Sep 28 '22 at 20:58
  • Yeah, it's usually a lot simpler, but since you have nested `whereHas()`, I'm not sure how it behaves... You _could_ do a separate query to get the count, then use that in `has('desks', $totalDesksCount)`. Alternatively, if you can post the migrations and Models/Relationships for your 4 tables, I can attempt to recreate and debug this too (tomorrow though). Thanks for editing the question and not posting your code in the comments; that's a breath of fresh air here on Stackoverflow – Tim Lewis Sep 28 '22 at 21:12
  • 1
    Thank you very much for taking the time to help! I'll continue also tomorrow to debug this ;) And no problem for the edit, it wouldn't have been readable in the comments... Here's a gist with the code you'd need to help me. https://gist.github.com/dib258/f12528b733cb0124edb70cb18e53816c – dib258 Sep 28 '22 at 21:48
  • Alrighty... So update on this, I wasn't able to get this working in a single Query. The counts are not scoped properly; like `$user->desks->count()` will return a count of _all_ desks, not a count of all desks in each office (since `office_id` is not on `possessed_desks`, and it wouldn't make sense there). I tried with new relationships, like `Office::withCount('desks_count')->has('userPossessedDesks', '=', 'desks_count')`, but that compares against the string `'desks_count'`, not the column alias from `withCount()`. And even then, it can't scope it on a per-office basis... – Tim Lewis Sep 29 '22 at 16:41
  • Also, as a note, the `PossessedDesk` Model is actually not needed; it can act as a Pivot between `User` and `Desk`, with a relationship on `User.php`: `public function desks() { return $this->belongsToMany(Desk::class, 'possessed_desks'); }`. It doesn't help or change anything with the code I attempted, but general practice is to _not_ define a Model for what is essentially a pivot table. Thanks again for the Gist; it was very helpful in attempting to debug this, but yeah; I think the approach you have is gonna have to work, unless someone smarter than me has any other solution – Tim Lewis Sep 29 '22 at 16:43
  • 1
    Okay, I'll keep what I came up with for now until the choosen one comes along. ^^' And I also realised that `PossessedDesk` should be a Pivot table, I'll try to change this later in my database when I have time. Thank you for the help! ;) – dib258 Sep 30 '22 at 17:43

0 Answers0