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

MrFiliper's avatar

Eloquent eager loading with condition loads first rows in the relationships table

When using the Eloquent with function to eager loading with when function where is user role check, it is always true because Eloquent get always the first record of the roles table (that this user is admin). This is not the problem when eager loading is not used and lazy load is used.

Steps To Reproduce: Models to simulate this issue is below, also using spatie/laravel-permission role system. But this is not with the role system, because I test it on any other relationship with the user and there is the same issue.

User.php

class User extends Authenticatable implements MustVerifyEmail
{
    use HasRoles, HasFactory, Notifiable;

    /**
     * The attributes that should be hidden for arrays.
     *
     * @var array
     */
    protected $hidden = [
        'password',
        'remember_token',
    ];

    /**
     * Send the email reset notification
     * @param string $token
     */
    public function sendPasswordResetNotification($token)
    {
        $this->notify(new ResetPasswordNotification($token));
    }

    /**
     * Send the email verification notification.
     *
     * @return void
     */
    public function sendEmailVerificationNotification()
    {
        $this->notify(new VerifyEmailNotification());
    }
}

Product.php

class Product extends Model
{
    use HasFactory, Searchable;

    protected $table = 'products';

    public function offers() {
        return $this->hasMany(Offer::class)->whereHas('shop', function ($query) {
            $query->where('is_active', '=', true);

            $user = auth()->user();# Could be also User::find(136);, but not working too

            $query->when($user != null && ($user->hasRole('admin')), function ($query) use ($user) {
                $query->orWhere('name', '=', 'adminshop');
            });

            return $query;
        });
    }
}

Offer.php

class Offer extends Model
{
    use HasFactory, SoftDeletes;

    protected $table = 'offers';

    public $dates = ['created_at', 'updated_at', 'deleted_at', 'last_seen_at'];

    public function shop() {
        return $this->belongsTo(Shop::class);
    }

    public function product() {
        return $this->belongsTo(Product::class);
    }

    public function price() {
        return $this->hasOne(OfferPrice::class)->orderByDesc('created_at');
    }

    public function getLastPrice() {
        $price = $this->price;

        if($price == null) {
            return 0;
        } else {
            return $price->price;
        }
    }
}

Category.php

class Category extends Model
{
    use HasFactory, Searchable;

    protected $table = 'categories';

    public function products() {
        return $this->belongsToMany(Product::class);
    }
}

When running the code below, it shows also offers from adminshop even when the user is not an admin.

$category->products()->with(['offers', 'offers.price']);

When using dd to dump data, the user is ok:

  #original: array:16 [▼
    "id" => 136
    "nickname" => "moderated"
    "name" => null
    "surname" => null
    "avatar" => null
    "email" => "moderated"
    "email_verified_at" => "2022-01-03 14:49:57"
    "password" => "moderated"
    "remember_token" => "moderated"
    "created_at" => "2022-01-03 14:47:00"
    "updated_at" => "2022-01-03 14:49:57"
  ]

but the relationships (this is the roles table - but it same for all relationships) are wrong because are not for that user!

      #original: array:8 [▼
        "id" => 1
        "name" => "admin"
        "guard_name" => "web"
        "created_at" => "2021-12-01 13:38:53"
        "updated_at" => "2021-12-01 13:38:53"
        "pivot_model_id" => 1 #<- THIS IS WRONG! supposed to be 136 or no result found
        "pivot_role_id" => 1
        "pivot_model_type" => "App\Models\User"
      ]

When loading lazy load relationships there is no problem and data is loaded correctly. When using edger loading, always is dumped the first rows in the relationships tables, but why?

0 likes
3 replies
Snapey's avatar

no, no

only return a relationship in the model you cannot run queries inside a relationship definition

MrFiliper's avatar

@Snapey So change it to this on the Product.php model

    public function offers() {
        return $this->hasMany(Offer::class)->where('last_seen_at', '>', Carbon::now()->addDays(-1));
    }

and in CategoryController call like this?

        $products = $category->products()->whereHas('offers.shop', function ($query) {
            $query->where('is_active', '=', true);

            $query->when(auth()->user() != null && (auth()->user()->hasRole('admin')), function ($query) {
                $query->orWhere('name', '=', 'adminshop');
            });

            return $query;
        });

And what about the lowestOffer method on the Product.php model, that not working because of changed logic? Now not recognize if is admin or not, add a new check to the method?

    public function lowestOffer() {
        $min = -1;

        foreach ($this->offers as $offer) {
            $price = $offer->getLastPrice();

            if($price == null) {
                continue;
            }

            if($min == -1) {
                $min = $price;
            }

            if($min > $price) {
                $min = $price;
            }
        }

        return $min;
    }
MrFiliper's avatar
MrFiliper
OP
Best Answer
Level 1

For anyone who has this issue with creating relationships with the condition, make only the relation definition of that model. For this example:

class Product extends Model
{
    public function offers() {
        return $this->hasMany(Offer::class);
    }
}

then in the controller, check if it is admin and create a query

        $isAdmin = auth()->user() != null && auth()->user()->hasRole('admin');

        $sets = $category->products()->with(['offers' => function($q) use ($isAdmin ) {
            $q->where('offers.last_seen_at', '>', Carbon::now()->addDays(-1));

            $q->whereHas('shop', function ($query) use ($isAdmin ) {
                $query->where('is_active', '=', true);

                $query->when($isAdmin, function ($query) use ($isAdmin ) {
                    $query->orWhere('shops.name', '=', 'adminshop');
                });

                return $query;
            });
        }, 'offers.price']);

Please or to participate in this conversation.