Select specific column from morph relation according to type in Laravel

Viewed 2794

I am trying to write a query that selects columns from a model then selects some columns from a morph relationship table. But I have no idea to select columns, and relation tables have different columns. So some column has no slug, some have.

public function index()
{
    $menus = Menu::whereActive(true)
        ->with([
            'menuable' => function ($q) {
                // This gives error if  there is no relation Pages model
                $q->whereActive(true)->select('pages.id', 'pages.slug');

                // Below not working
                // if($q->type === Page::class){
                //    $q->whereActive(true)->select('pages.id', 'pages.slug');
                // } else if($q->type === Category::class){
                //     $q->whereActive(true)->select('categories.id', 
                           'categories.slug');
                // }
            }
        ])
        ->get(['id', 'menuable_id', 'menuable_type', 'name']);

    $response = [
        'menus' => $menus,
    ];

    return $this->sendResponse($response);
}

Models

class Menu extends Model
{
    public function menuable()
    {
        return $this->morphTo();
    }
}

class Page extends Model
{
    public function menu()
    {
        return $this->morphOne(Menu::class, 'menuable');
    }
}

class Category extends Model
{
    public function menu()
    {
        return $this->morphOne(Menu::class, 'menuable');
    }
}

How can I select specific columns from morph relation with checking morph type? I am using Laravel version 8.

2 Answers

The polymorphic relation is something the Eloquent aware of, and DBMS hasnot implemented this feature in it. so there cannot be a sql query which join a table to another tables based on the morph column.

so you have to use distinct queries for every polymorphic join relation on your models:

//you can retrieve distinct menu based on their relation 
Menu::whereActive(true)->hasPage()->with('pages');

//and having the ralations in the menu model:
public function posts
Menu::whereActive(true)->hasCategory();

//scope in menu class can be like:
public function scopePage($query){
    return $query->where('menuable_type',Page::class);
}
public function scopeCategory($query){
    return $query->where('menuable_type',Category::class);
}

//with these you can eager load the models
Menu::whereActive(true)->hasPage()->with('page');
Menu::whereActive(true)->hasCategory()->with('category');

public function page(){
    return $this->belongsTo(Page::class);
}
public functioncategory(){
    return $this->belongsTo(Category::class);
}

if you want a common interface to use one of these dynamically. you can use:

$menu->menuable->id
$menu->menuable->slug

I am not sure which columns you want to response, but as i can guess from your question, I suppose you want id and slug from both models.

public function index(){
    $pagedMenu = Menu::whereActive(true)->hasPage()->with('page');
    $categoriedMenu = Menu::whereActive(true)->hasCategory()->with('category');

    $menues = $pagedMenu->merge($categoriedMenu);
    
    $response = [
        'menus' => $menus,
    ];

    return $this->sendResponse($response);
}

You can perfom separate filters based on the morph-class. This can be achieved with the whereHasMorph (https://laravel.com/docs/8.x/eloquent-relationships#querying-morph-to-relationships).

If you need to automatically resolve the relationship you can use with. This will preload morphables automatically into your resulting collection.

The following example uses an orWhere with 2 separate queries per morphable.

$menus = Menu::whereActive(true)

    ->whereHasMorph('menuable', [Page::class], function (Builder $query) {
        // perform any query based on page-related entries
    })

    ->orWhereHasMorph('menuable', [Category::class], function (Builder $query) {
        // perform any query based on category-related entries
    })

    ->with('menuable')

  ;

An alternative way is to pass both classes to the second argument. In that case you can check the type inside your closure.

$menus = Menu::whereActive(true)

    ->whereHasMorph('menuable', [Page::class, Category::class], function (Builder $query, $type) {

        // perform any query independently of the morph-target
        // $q->where...

        if ($type === (new Page)->getMorphClass()) {
            // perform any query based on category-related entries
        }

        if ($type === (new Category)->getMorphClass()) {
            // perform any query based on category-related entries
        }
    })

    ->with('menuable')

If required you can also preload nested relationships. (https://laravel.com/docs/8.x/eloquent-relationships#nested-eager-loading-morphto-relationships)

$menus = Menu::whereActive(true)

    ->with(['menuable' => function (MorphTo $morphTo) {
        $morphTo->morphWith([
            Page::class => ['calendar'],
            Category::class => ['tags'],
        ]);
    }])
Related