PeteBatin's avatar

Only where nested relation exists?

Hi all,

Apologies if this has already been asked/covered (I promise I did look before posting!).

I have 4 models, Clients, Projects, Budgets and Timeentries.

Each Client can have multiple Projects, each project can have multiple Budgets and each Project can have multiple Timeentries logged against them.

I the lookup needs the following criteria

  1. Only Client records that have Projects
  2. And only where Projects have Budgets assigned to them
  3. And then work out from the Timeentries how much of the Budget has been used.

I've kind of achieved the above but Projects are being pulled out regardless of whether they have budgets or not. The aim is to produce a daily notification of projects that are approaching or have exhausted their budget.

Models below (I inherited the database so I'm taking no blame for the column naming conventions used).

app\Models\Clients

class Clients extends Model
{

    public function timeentries() : HasMany{
        return $this->hasMany(Timeentries::class, 'Client', 'Code');
    }

    public function projects():HasMany{
        return $this->hasMany(Projects::class, 'Client', 'Code');
    }
}

app\Models\Projects

class Projects extends Model
{
    public function clients(): BelongsTo
    {
        return $this->belongsTo(Clients::class, 'Client', 'Code');
    }

    public function client() : HasOne
    {
        return $this->hasOne(Clients::class, 'Client', 'Code');
    }

    public function timeentries(): HasMany
    {
        return $this->hasMany(Timeentries::class, 'Project', 'Code');
    }

    public function budgets() : HasMany
    {
        return $this->hasMany(Budget::class, 'Project', 'Code');
    }
}

app\Models\Budgets

class Budgets extends Model
{

    public function project() : BelongsTo{
        return $this->belongsTo(Projects::class, 'Project', 'Code');
    }
}

app\Models\Timeentries

class Timeentries extends Model
{
    public function project():BelongsTo{
        return $this->belongsTo(Projects::class, 'Project', 'Code');
    }


    public function client():BelongsTo{
        return $this->belongsTo(Clients::class, 'Client', 'Code');
    }

    public function employee():BelongsTo{
        return $this->belongsTo(Employees::class, 'Employee', 'Code');
    }

    public function getProject() : HasOne{
        return $this->hasOne(Projects::class, 'Code', 'Project');
    }


    public function getEmployee() : HasOne{
        return $this->hasOne(Employees::class, 'Code', 'Employee');
    }
}

This is my query so far:

$clients = Clients::where('Active', 'Y')
            ->with([
                'projects' => [
                    'budgets',
                    'timeentries',
                ],
            ])
            ->whereHas('projects', function(Builder $query){
                $query->where('Active', 'Y');
            })
            ->has('projects.budgets')
            ->orderBy('name', 'asc')
            ->get();

The above will list all projects of a client whether the project has a budget(s) set for it or not. I want only the ones that have a budget set.

I have a hunch I need an additional whereHas that specifies projects.budgets not null or similar but can't quite work that out. Any ideas/suggestions appreciated!

At the moment I'm having to filter within my foreach loops, there has to be a better way.

foreach($clients as $client){
            foreach($client->projects as $project){
                if($project->budgets->count()){

                    $totalBudget = $project->budgets->sum('Cost');
                    $totalBudgetTime = $project->budgets->sum('Time');

                    dump($project->Code.' '.$project->name.' - BudgetValue: '.$totalBudget.' - BudgetTime: '.$totalBudgetTime);
                }
            }
        }
0 likes
8 replies
Sergiu17's avatar
Level 60

This should do

$clients = Clients::where('Active', 'Y')
    ->withWhereHas('projects', function(Builder $query){
        $query->withWhereHas('budgets')->with('timeentries')->where('Active', 'Y');
    })
    ->orderBy('name', 'asc')
    ->get();
1 like
PeteBatin's avatar

@Sergiu17 gave it a try but I'm getting

Call to undefined method App\Models\Clients::projects()
Sergiu17's avatar

@PeteBatin Open php artisan tinker and start crafting this query one step at the time


Clients::withWhereHas('projects')
    ->orderBy('name', 'asc')
    ->get();

// make sure it works

$clients = Clients::where('Active', 'Y')
    ->withWhereHas('projects', function(Builder $query){
        $query->where('Active', 'Y');
    })
    ->orderBy('name', 'asc')
    ->get();

// make sure it works


$clients = Clients::where('Active', 'Y')
    ->withWhereHas('projects', function(Builder $query){
        $query->withWhereHas('budgets')->where('Active', 'Y');
    })
    ->orderBy('name', 'asc')
    ->get();

// and so one
1 like
PeteBatin's avatar

@Sergiu17 Thank you, I found the issue!

So first issue (the one I reported previously) was that I have two tables called clients (separate database connections) and the one in question here has a prefix which I left off to keep things simple for illustration purposes.....only I didn't add the prefix when copying your code (sorry!!).

So that fixed, I then got a different error

{closure}(): Argument #1 ($query) must be of type Illuminate\Database\Eloquent\Builder, Illuminate\Database\Eloquent\Relations\HasMany given, called in vendor\laravel\framework\src\Illuminate\Database\Eloquent\Concerns\QueriesRelationsh ips.php

I removed Builder and it worked!

Why would Builder cause the problem above?

Sergiu17's avatar

@PeteBatin weird case, there's some delegation between withWhereHas and has and with methods, remove the type hints or add them both

->withWhereHas('projects', function(Builder|HasMany $query) {
1 like
PeteBatin's avatar

@Sergiu17 The type hints worked, thanks Sergiu, really appreciate your assistance today!

1 like
Sergiu17's avatar

@PeteBatin wait :D

use this

use Illuminate\Contracts\Database\Eloquent\Builder;

->withWhereHas('projects', function(Builder $query) {
PeteBatin's avatar

@Sergiu17 Hope you had a good weekend!

That also works (with the HasMany type hint removed too).

Although VS Code (Intelephense) now complains of "Undefined method 'withWhereHas'" but it does work.

Side question..., is there somewhere I can read about the differences in the traits? My biggest gripe with Laravel is the documentation.

Please or to participate in this conversation.