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

croftCoder's avatar

SQL Query To Eloquent Relationship

// category-product_variant pivot table
Schema::create('category_product_variant', function (Blueprint $table) {
    $table->id();
    $table->foreignId('category_id')->constrained('categories');
    $table->foreignId('product_variant_id')->constrained('product_variants');
    $table->timestamps();
    $table->softDeletes();

    // unique constraints
    $table->unique(['category_id', 'product_variant_id'], 'category_product_variant_unique');
});

// category model
class Category extends Model
{
    public function products()
    {
        // return $this->hasMany(Product::class); <-- only gets products directly connected to the category

        // WHAT TO DO HERE
    }
}

// raw sql
select * from `products`
where `category_id` = 6
   or exists (
       select * from `product_variants`
       where `products`.`id` = `product_variants`.`product_id`
         and exists (
             select * from `categories`
             inner join `category_product_variant`
             on `categories`.`id` = `category_product_variant`.`category_id`
             where `product_variants`.`id` = `category_product_variant`.`product_variant_id`
               and `categories`.`id` = 6
               and `categories`.`deleted_at` is null
         )
       and `product_variants`.`deleted_at` is null
   )
and `products`.`deleted_at` is null

i have the following models: Product, ProductVariant and Category. a Product belongs to a Category via category_id, it also has many ProductVariants via product_id on the product_variants table. a ProductVariant may have categories attached to it stored in a m2m pivot table category_product_variant. the raw sql above produces the desired results or data. i want to get all products in a category even if it's only the variant that's associated with it. for example a book. the book has the books category and one of its variants is connected to the audiobooks category. i want the book to show when i look for products in the audiobooks category.

public function products()
{
    $categoryId = $this->id;

    return Product::where('category_id', $categoryId)
        ->orWhere(function ($query) use ($categoryId) {
            $query->whereExists(function ($subquery) use ($categoryId) {
                $subquery->select(DB::raw(1))
                    ->from('product_variants')
                    ->whereColumn('products.id', 'product_variants.product_id')
                    ->whereExists(function ($subquery2) use ($categoryId) {
                        $subquery2->select(DB::raw(1))
                            ->from('categories')
                            ->join('category_product_variant', 'categories.id', '=', 'category_product_variant.category_id')
                            ->whereColumn('product_variants.id', 'category_product_variant.product_variant_id')
                            ->where('categories.id', $categoryId)
                            ->whereNull('categories.deleted_at');
                    })
                    ->whereNull('product_variants.deleted_at');
            });
        })
        ->whereNull('products.deleted_at');
}

this returns this error: App\Models\Category::products must return a relationship instance.

0 likes
7 replies
Tray2's avatar

You don't have to use Eloquent/Query Builder, you can just do a DB::select()

croftCoder's avatar

@tray2

public function products()
{
    $categoryId = $this->id;

    return DB::select(
        "select * from `products`
        where `category_id` = ?
        or exists (
            select * from `product_variants`
            where `products`.`id` = `product_variants`.`product_id`
            and exists (
                select * from `categories`
                inner join `category_product_variant`
                on `categories`.`id` = `category_product_variant`.`category_id`
                where `product_variants`.`id` = `category_product_variant`.`product_variant_id`
                and `categories`.`id` = ?
                and `categories`.`deleted_at` is null
            )
            and `product_variants`.`deleted_at` is null
        )
        and `products`.`deleted_at` is null", [$categoryId, $categoryId]
    );
}

i get this error: App\Models\Category::products must return a relationship instance.

Tray2's avatar

@croftCoder That is exactly what I meant. I guess you are trying to do some kind of filtering here on the category, then I would use when in the controllers index method instead.

Something like this.

 public function __invoke(Request $request)
    {
        return view('books.index')
            ->with([
                'books' => BookIndexView::query()
                    ->when($request['authors'], function ($query, $authors) {
                        $query->whereIn('author_id',
                            $this->numericStringToArray($authors));
                    })
                    ->when($request['published'], function ($query, $published) {
                        $query->where('published_year', $published);
                    })
                    ->when($request['genre'], function ($query, $genre) {
                        $query->where('genre', $genre);
                    })
                    ->when($request['format'], function ($query, $format) {
                        $query->where('format', $format);
                    })
                    ->when($request['search'], function ($query, $search) {
                        $query->where('title', 'LIKE',  "%$search%")
                        ->orWhere('author_name', 'LIKE', "%$search%")
                        ->orWhere('series', 'LIKE', "%$search%");
                    })
                    ->orderBy('author_name')
                    ->orderBy('series')
                    ->orderBy('part')
                    ->orderBy('published_year')
                    ->get(),
            ]);
    }
rodrigo.pedra's avatar

Well, you are not returning a relationship instance...

It could look something like:

public function products()
{
    $categoryId = $this->id;
    
    // start with a relation
    return $this->hasMany(Product::class, 'category_id')
        ->orWhere(function ($query) use ($categoryId) {
            $query->whereExists(function ($subquery) use ($categoryId) {
                $subquery->select(DB::raw(1))
                    ->from('product_variants')
                    ->whereColumn('products.id', 'product_variants.product_id')
                    ->whereExists(function ($subquery2) use ($categoryId) {
                        $subquery2->select(DB::raw(1))
                            ->from('categories')
                            ->join('category_product_variant', 'categories.id', '=', 'category_product_variant.category_id')
                            ->whereColumn('product_variants.id', 'category_product_variant.product_variant_id')
                            ->where('categories.id', $categoryId)
                            ->whereNull('categories.deleted_at');
                    })
                    ->whereNull('product_variants.deleted_at');
            });
        })
        ->whereNull('products.deleted_at');
}

But using the instance id, as you do when running $categoryId = $this->id; will prevent eager loading related models.

You could either:

  • write your own \Illuminate\Database\Eloquent\Relations\Relation subclass, and take care of the abstract methods
  • do as @tray2 suggested and get the models from an SQL query

What I believe @tray2 meant, is not to have a custom SQL to define a relation, but get the related models through an SQL query, something like this:

// use question marks (?)  for value binding to prevent SQL injection
$sql = <<<'SQL'
SELECT *
FROM ...
SQL;

// one binding to each question mark (?)
$products = Product::query()->fromQuery($sql, [
    $category->id,
    /* any other needed binding */
]);
croftCoder's avatar

@rodrigo.pedra

public function products()
{
    $category_id = $this->id;

    $sql = <<<'SQL'
    SELECT * FROM `products`
    WHERE `category_id` = ?
        OR EXISTS (
            SELECT * FROM `product_variants`
            WHERE `products`.`id` = `product_variants`.`product_id`
            AND EXISTS (
                SELECT * FROM `categories`
                INNER JOIN `category_product_variant`
                ON `categories`.`id` = `category_product_variant`.`category_id`
                WHERE `product_variants`.`id` = `category_product_variant`.`product_variant_id`
                AND `categories`.`id` = ?
                AND `categories`.`deleted_at` IS NULL
            )
            AND `product_variants`.`deleted_at` IS NULL
        )
    AND `products`.`deleted_at` IS NULL
    SQL;

    $products = Product::query()->fromQuery($sql, [$category_id, $category_id]);
    // dd($products); // <-- THIS RETURNS PRODUCTS FROM THE DATABASE
    return $products;
}

but i get this error: App\Models\Category::products must return a relationship instance.

rodrigo.pedra's avatar

@croftCoder the suggestion was to use the fromQuery() on where you need this complex query, for example, on a controller, a job, or whatever. That code is not meant to be returned as a relationship, as from what I explained earlier, a relationship method must return a \Illuminate\Database\Eloquent\Relations\Relation subclass.

If you need to set this as a relationship, take the other one and write a custom \Illuminate\Database\Eloquent\Relations\Relation subclass.

Or test the first code snippet I posted, the one which starts with return $this->hasMany(Product::class, 'category_id'). But be aware you won't be able to properly eager load models with that.

rodrigo.pedra's avatar

Creating a custom \Illuminate\Database\Eloquent\Relations\Relation subclass is not that much work, as it seems.

And you can use the many relation subclasses shipped with Laravel as an inspiration.

Please or to participate in this conversation.