Be part of JetBrains PHPverse 2026 on June 9 – a free online event bringing PHP devs worldwide together.

SigalZ's avatar

4 tables - relationship - query

Hello,

I have these 4 tables:

origins, countries, green_beans, coffees

Origin has many Countries:

public function countries()
{
       return $this->hasMany(Country::class);
 }

public function greenBeans()
{
        return $this->hasManyThrough(GreenBean::class, Country::class);
}

Country has many Green Beans:

public function origin()
{
       return $this->belongsTo(Origin::class);
 }

 public function greenBeans()
 {
     return $this->hasMany(GreenBean::class);
 }

Green Bean has many Coffees:

public function country()
 {
        return $this->belongsTo(Country::class);
 }
   
public function coffees()
{
      return $this->hasMany(Coffee::class);
}

The coffees table has an 'active' column.

On my page I need to display only Origins that have active coffees in them.

In each origin I need to show the Countries ordered by name

Under each country I need to show the coffee name and description

Something like:

@foreach($origins as $origin) {
    <h3>{{ $origin->name }}</h3>
     @foreach($countries->as $country)
        <h4>{{ $country->name }}</h4>
		@foreach($coffees as $coffee)
         <div>
            <h4 class="anton-font">{{ $coffee->name }}</h4>
            {!! $coffee->coffee_desc !!}
          </div>
      @endforeach
   @endforeach
@endforeach

I created this function in the Origin model:

public function scopeHasCoffees($query)
{
        return $query->select('origins.name')
                ->join('countries', 'origins.id', '=', 'origin_id')
                ->join('green_beans', 'countries.id', '=', 'country_id')
                ->rightJoin('coffees', 'green_bean_id', '=', 'green_beans.id')
                ->where('coffees.active', 1)
                ->groupBy('origins.name');
 }

But now I'm stuck and have no clue how to continue from here.

I get the correct origins but how do I get the rest of the data I need?

0 likes
19 replies
webrobert's avatar

why not use the power of the relationships? over joins?

edit: fixed a typo.

$origins = Origin::query()
	->whereHas('greenBeans', fn($q) => $q->where('active', 1))
	->with('countries','greenBeans.coffees')
	->get();

have a look...

dd(
  $origins->toArray()
);
SigalZ's avatar

@webrobert Thx! Can you help a bit more please? How do I get the active coffees? The green_beans table does not have 'active' field in it. I tried this:

$origins = Origin::whereHas('greenBeans')
	->with('countries','greenBeans.coffees', fn($q) => $q->where('coffees.active', 1))
	->get();

Gives me an error:

mb_strpos() expects parameter 1 to be string, object given

webrobert's avatar

@SigalZ, add the condition in the whereHas like my snippet. (I corrected a typo on the query() bit I do that for sugar

SigalZ's avatar

@webrobert I cannot add it on the whereHas, the field is in the coffees table not in the green_beans table.

webrobert's avatar

@SigalZ

ahh.. you want the coffees. I think you can still do

	->whereHas('greenBeans.coffees', fn($q) => $q->where('active', 1))

or

   ->whereRelation('greenBeans.coffees', 'active', 1)

so

$origins = Origin::query()
    ->whereRelation('greenBeans.coffees', 'active', 1)
	->with('countries','greenBeans.coffees')
	->get();
SigalZ's avatar

@webrobert That didn't work well. Now I get the countries and the beans under the origins. So I can't tell which country the bean belongs to. So each country under an origin gets all the beans and coffees under it.

For example: Bean: Barbarosa Gold belongs to Brazil but I see it under all the countries.

So I see this:

Origin: South America
Country: Brazil
Bean: Barbosa Gold
Coffee: Brazil
Country: Colombia
Bean: Barbosa Gold
Coffee: Brazil
Country: Peru// (Peru doesn't even have active coffees under it)
Bean: Barbosa Gold
Coffee: Brazil

So I changed the query to:

$origins = Origin::whereHas('countries.greenBeans.coffees', fn($q) => $q->where('coffees.active', 1))
	                ->with('countries.greenBeans.coffees')
	                ->get();

but now I get countries that don't have active coffees in them.

Arggggg

webrobert's avatar

@SigalZ

Just active. You are already at the coffees…

Origin::whereHas('countries.greenBeans.coffees', fn($q) => $q->where('active', 1))

SigalZ's avatar

@webrobert Thx, but it doesn't matter, it doesn't change anything. I still get Peru that has no active coffees under it. That's the sql query that it produces:

select * from `origins` where exists (select * from `countries` where `origins`.`id` = `countries`.`origin_id` and exists (select * from `green_beans` where `countries`.`id` = `green_beans`.`country_id` and exists (select * from `coffees` where `green_beans`.`id` = `coffees`.`green_bean_id` and `active` = 1) and `green_beans`.`deleted_at` is null) and `countries`.`deleted_at` is null) and `origins`.`deleted_at` is null
webrobert's avatar

@SigalZ, timeout..

Does this return the correct coffees?

$origins = Origin::query()
    ->whereRelation('greenBeans.coffees', 'active', 1)
	->with('greenBeans.coffees')
	->get();
SigalZ's avatar

@webrobert Nope. Still getting coffees with active = 0.

The sql query:

select * from `origins` 
where exists (select * from `green_beans` inner join `countries` on `countries`.`id` = `green_beans`.`country_id` 
where `origins`.`id` = `countries`.`origin_id` 
and exists (select * from `coffees` where `green_beans`.`id` = `coffees`.`green_bean_id` and `active` = 1)
and `green_beans`.`deleted_at` is null and `countries`.`deleted_at` is null) and `origins`.`deleted_at` is null

I don't understand why it ignores the part and 'active' = 1. I guess that's why I have to use join and not exists in the sql query.

I guess I will need to do it in a few steps.

Thank you so much for trying to help

Sinnbeck's avatar

@SigalZ So I tried this query

select * from `origins` where exists (select * from `countries` where `origins`.`id` = `countries`.`origin_id` and exists (select * from `green_beans` where `countries`.`id` = `green_beans`.`country_id` and exists (select * from `coffees` where `green_beans`.`id` = `coffees`.`green_bean_id` and `active` = 1) and `green_beans`.`deleted_at` is null) and `countries`.`deleted_at` is null) and `origins`.`deleted_at` is null

And got 3 rows. No Peru?

https://i.imgur.com/odHCLwU.png

Tested on MariaDB 10.3.27

Are you in reality talking about relationships (countries)? If so, you need to have the same whereHas() query for them

If so then

$origins = Origin::query()
	->whereHas('greenBeans', fn($q) => $q->where('active', 1))
	->with(['countries' => function ($query) {
            $query->whereHas('greenBeans', fn($q) => $q->where('active', 1)) //fixes countries
      }, 'greenBeans' =>  function ($query) {
            $query->where('active', 1)->with('coffees'); //fixes green beans
      })
	->get();
SigalZ's avatar

@Sinnbeck Thx, The raw query came from one of the Eloquent queries I tried to run in a controller.

When you run it straight as an sql query, then yes, you get only the origins, but I need the countries and green beans and active coffees to be displayed, so If you run it in a controller with an eloquent query you get Peru, and Peru has no active coffees in it.

The relationship query you gave won't work either, green_beans do not have 'active' field. I need active coffees not green beans.

Sinnbeck's avatar
Sinnbeck
Best Answer
Level 102

@SigalZ Ok got it. I tried setting up the project and this is what I cam up with. Hope its correct.

    $origins = \App\Models\Origin::query()
        ->whereHas('greenBeans.coffees', fn($q) => $q->where('active', 1))
        ->with(['countries' => function ($query) {
            $query->whereHas('greenBeans.coffees', fn($q) => $q->where('active', 1))
            ->with(['greenBeans' =>  function ($query) {
                $query->whereHas('coffees',  fn($q) => $q->where('active', 1))
                    ->with(['coffees' =>  fn($q) => $q->where('active', 1)]);
            }]);
        }])
        ->get();

Hope it works as you expect :)

1 like
SigalZ's avatar

@Sinnbeck OMG!!!!!!! You solved it!!!! I now pronounce you the King of Laravel Relationships.

I'm on vacation, having fun, but now, you made my day!!!

I can't thank you enough.

Next time you're in South Africa, coffee is on me :)

1 like
Sinnbeck's avatar

@SigalZ Hehe happy to help and thank you so much for the title :) Have a great vacation

2 likes
SigalZ's avatar

@furqanDev Yes, I read about hasManyThrough, and you can see in my post that I use it in the Origin model. But after that, I don't know what to do.

I'll have a look at the package you linked.

Thank you very much.

SigalZ's avatar

Hello again, following a different post I had here, I decided to change my database structure and now I'm struggling again to get the data I need.

So now I have these tables:

Products table will hold fields that will be mutual to all products in the system and will have polymorphic relationships to other tables.

products table: name, product_desc etc. and productable_type, productable_id

The Product model has this function:

public function productable()
{
        return $this->morphTo();
}

One type of a product will be Bean:

beans table: id, green_bean_id, roast_type_id, etc.

Model:

public function products()
    {
        return $this->morphMany(Product::class, 'productable');
    }

green_beans table: id, name, country_id

Model:

public function country()
    {
        return $this->belongsTo(Country::class);
    }    

    public function beans()
    {
        return $this->hasMany(Bean::class);
    }

countries table: id, name, origin_id

Model:

public function origin()
    {
       return $this->belongsTo(Origin::class);
    }

    public function greenBeans()
    {
        return $this->hasMany(GreenBean::class);
    }

origins table: id, name

Origin model:

public function countries()
    {
       return $this->hasMany(Country::class)->orderBy('name');
    }

    public function greenBeans()
    {
        return $this->hasManyThrough(GreenBean::class, Country::class);
    }

I need to find origins that have beans that were not soft_deleted.

I tried all kinds of codes, but I get lost all the time, the last code I tried:

$o = Origin::with([
     'greenBeans' => fn($gb) => $gb->with(['beans' => fn($b) => $b->with(['products', fn($p) => $p->whereNull('deleted_at')
     ->whereHas('products', fn($q) => $q->whereNull('deleted_at'))
     ])
     ])
     ])
 ->get();

And I get this error:

mb_strpos() expects parameter 1 to be string, object given

I tried to find a tutorial that will help me with all these with and whereHas etc. but I can't find something that helps me with my specific needs and I get lost for hours now.

Please, can someone help?

SigalZ's avatar

I might have found the solution:

 $o = Origin::with(['greenBeans.beans.products' => fn($query) => $query->whereNull('deleted_at')])
 ->whereHas('greenBeans.beans.products', fn ($query) => 
 $query->whereNull('deleted_at')
 )
 ->get();

This seems to work, I'm just not sure it's the best way to do it.

Please or to participate in this conversation.