Framework: whereHas() not working with MorphTo()

Created on 12 Aug 2014  Â·  75Comments  Â·  Source: laravel/framework

The whereHas() method for querying a polymorphic relationship doesn't seem to be working.

Here are the relationships:

Transaction.php

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

Reservation.php

public function transactions()
    {
        return $this->morphMany('Transaction', 'transactionable');
    }

Project.php

public function transactions()
    {
        return $this->morphMany('Transaction', 'transactionable');
    }

And here is what I am trying to do:

return Transaction::whereHas('transactionable', function($q){
        return $q->where('id', '=', '1');
    })->get();

Result:

SQLSTATE[42S22]: Column not found: 1054 Unknown column 'transactions.' in 'where clause' (SQL: select * from `transactions` where (select count(*) from `transactions` where `transactions`.`transactionable_id` = `transactions`.`` and `id` = 1) >= 1)

Is this possible?

bug

Most helpful comment

I came up with BelongsToMorph relationship type, and got whereHas working. Obviously, it only works on a single type, so you need to have explicit relationship declaration for each type.

// Model
class Comment extends Model
{
    public function post()
    {
        return BelongsToMorph::build($this, Post::class, 'commentable');
    }

    public function note()
    {
        return BelongsToMorph::build($this, Note::class, 'commentable');
    }
}

Source: https://gist.github.com/xxzefgh/3022fee8afa53e45a6b89da3f16b3815

All 75 comments

Did you managed to get this working? I'm having the same problem.

Yep, having the same problem!

same here

Running into the same issue. It doesn't even seem to look at the morph type, it just does a select from the same table.

Any more information on it?

I have the same problem. Very sad.

I ran the code:

$tasksQuery->whereHas('taskable', function ($q) use ($search) {
    $q->where('title', 'like', "%{$search}");
});

but it throw an exception.

I reaallly need this asap, so I'm going to look into it in a few days and try to debug it. I don't know anything about the code, so if anyone could clarify what I should be looking for, go ahead.

Yup same here. Wonder if this was ever working?

I think this kinda impossible currently, because if you see the MorphTo class were derived from BelongsTo relation class, but in BelongsTo model relationship declaration of the model or foreign key were explicitly configured, whereby in MorphTo relationship is not pointing to related model.

This polymorphism relationship only fully capable when the model instance/object were loaded whereby the data_type value that pointing to related model class can be obtained. And in whereHas situation, the builder class need to gather all the query related information such as filter, binding and etc to be use later on query generation and execution. It's impossible to create a queries without knowing the kind row/data type that will be retrieve later

You can have a look at file Eloquent\Relations\BelongsTo.php, method 'getRelationCountQuery' this is where everything is falling apart.

I'm sure @taylorotwell and others core developer were fully aware about this issue, but it might be too hacky to make this possible. Anyway it's just my opinion.

Looks like this is a no-fix then.

Err. Any workaround then?

What about throwing a better exception when it happens? Also would like to
see a work-around :-) Also, what about maybe passing a parameter that's an
array of possible table/model names?

On Thu, Feb 5, 2015 at 10:52 AM, Sebastiaan Luca [email protected]
wrote:

Err. Any workaround then?

—
Reply to this email directly or view it on GitHub
https://github.com/laravel/framework/issues/5429#issuecomment-73103451.

You could try to have the Builder class to look ahead for the value of the morphed relation class. Code provided is the only proof of concept, it's not a workaround or a suitable solution. Please do not use in your code/project.

As you can see the line with variable $look_ahead is trying to fetch($parent::first) the data/model instance and get a morphed relation of it.

illuminate/database/Illuminate/Database/Eloquent/Builder.php

    protected function getHasRelationQuery($relation_name)
    {
        return Relation::noConstraints(function() use ($relation_name)
        {
            $relation =  $this->getModel()->$relation_name();

            if( get_class($relation) == 'Illuminate\Database\Eloquent\Relations\MorphTo'){
                $parent = $relation->getParent();
                $look_ahead = $parent::first();
                $relation = $look_ahead->{$relation_name}();
            };

            return $relation;
        });
    }

By using this the whereHas execution will return a value without failing, although it might not returning the right data due to the poor look ahead mechanism. It might be a good start point to explore other possibilities.

Thanks @wajatimur! By using an adjusted version of your code, I can use whereHas by providing the …able_type. It only works for a single type, but in a sense, that's only logical. Does what it needs to for me.

/**
 * Get the "has relation" base query instance.
 *
 * @param  string  $relation
 * @return \Illuminate\Database\Eloquent\Builder
 */
protected function getHasRelationQuery($relation)
{
    return Relation::noConstraints(function() use ($relation)
    {
        $name = $relation;
        $relation = $this->getModel()->$relation();

        if (get_class($relation) === 'Illuminate\Database\Eloquent\Relations\MorphTo') {
            $lookAhead = $this->getModel()->where($name . '_type', '=', 'App\Models\Auth\Users\Admin')->first();
            $relation = $lookAhead->{$name}();
        };

        return $relation;
    });
}

Feedback is appreciated.

Hi @Quagh, glad its work! A whereHas method will construct COUNT SQL query for each level of whereHas query _plus_ with WHERE query using addWhere method in Builder class if I'm were not mistaken.

By not having to scheme deeper on the Builder class code and i think the only possible solution is to place a placeholder in the generated query and it will refer back to some special method such as getHasRelationQueryMorphed to get an appropriate sub query to be inject. But anyway, its just a theory.

I'm not really following when you mention to put a placeholder in the generated query. Can you elaborate on that?

Anyway, I forked the Laravel repo and applied my changes to them. If anyone's interested, check out https://github.com/Quagh/framework/tree/database/fix_whereHas_with_morphTo.

Here's an example of how to use it. You just pass a string or array of strings of types it should look for. In my opinion, that's the only workable way the query builder can gather all the necessary elements to apply the whereHas. Another way would be to query the whole table, check for all different able_types, find the models, check the fields/relationships on there, etc. But not really do-able.

 $this->whereHas('userable', function ($query) use ($id) {
    $query->where('id', '=', $id);
}, '>=', 1, ['App\Models\Contact', 'App\Models\Admin']);

Composer.json:

"require": {
    "laravel/framework": "dev-database/fix_whereHas_with_morphTo as 4.2.17"
},

"repositories": [
    {
        "type": "vcs",
        "url": "https://github.com/Quagh/framework.git"
    }
],

And this is the query it produces when executing the code above (in my app);

select * from `users` where `users`.`deleted_at` is null and ((select count(*) from `admins` where `users`.`userable_id` = `admins`.`id` and `id` = ? and `admins`.`deleted_at` is null and `users`.`userable_type` = ?) >= 1 or (select count(*) from `contacts` where `users`.`userable_id` = `contacts`.`id` and `id` = ? and `contacts`.`deleted_at` is null and `users`.`userable_type` = ?) >= 1)

As you see, it wraps a single whereHasquery in parenthesis. This to make sure where 'users'.'deleted_at' is null applies to all has queries when providing multiple able types.

Trying to run phpunit tests right now to verify it doesn't break anything else. I don't have much experience writing tests myself, so if anyone feels like it, go right ahead :)

Again, feedback / testing appreciated.


OK (1349 tests, 2777 assertions)


@wajatimur @GrahamCampbell

:+1: Same here

Yep! Same here!

I worked out a stupid solution:

Comment morphTo Post and Tutorial:

public function scopeCommentableExists($query)
{
    return $query->where(function ($query) {
        $query->where(function ($query) {
                $query->where('commentable_type', 'Post')
                    ->has('post');
            })
            ->orWhere(function ($query) {
                $query->where('commentable_type', 'Tutorial')
                    ->has('tutorial');
            });
    });
}

Nice try @blankhua, that's a hard-coded recursive query. Anyway can anybody confirm, does this issue still exist in Laravel 5?

Still having this issue in Laravel 5

I'm also seeing the same issue in L5 (5.0.30). Would be great to have an official fix soon as the bugs been around for a while. Thanks to @Quagh for the interim solution though - I'll check that out

We're open to pull requests. :)

Would love to but it's a level of voodoo slightly above my pay grade! Sorry if I sounded demanding... ; )

@GrahamCampbell I think I can do a pull request, though I'd love some feedback on my implementation. It requires an extra variable (and clutters the morph class a bit), but it's the only viable solution I've come up with so far. It works though, which I think is the most important part :)

@Quagh Just send the PR and you'll get feedback :wink:

Are there any news for the PR? :(

Haven't really had time to look into it. My code's available so if anyone wants to have a go at it… Should work out-of-the-box with some cleanup.

@nkeena, please correct me if I'm wrong, but in your example, you are trying to find the transactions of both Reservation and Project with id = 1. Is that right? You could do the same simply by querying for
transactionable_id in your Transactions table.

If you know which model you want to search, you can use a join instead of a whereHas. Following @nkeena's example:

return Transaction::join('reservations', 'transactions.transactionable_id', '=', 'reservations.id')
    ->where('reservations.id', 1);
})->get();

I'm kind of pressed with time, but as soon as I can I'll post a use case similar to what I faced.

So, no official fix for this? :(
Now I hate polymorphic tables :P

@Aragami23 this is current limitation on this feature.

@pedromanoel not working on polypmorhic as relation between table is dynamic were determined upon query.

not yet???? ;(
Now I hate polymorphics also

I don't think there's any other solution than to pass it a list of models the query has to check for. See my example https://github.com/laravel/framework/issues/5429#issuecomment-74334956.

@sebastiaanluca, thanks for the comment. Is that solution fit to laravel 5.1.*?

@dehypnosis I suppose, but haven't tested it as I didn't need the functionality anymore.

As this thread was closed, but I needed polymorph support for whereHas method I have adopted @sebastiaanluca code in his pull request to be compatible with Laravel 5.2.

Here is my gist https://gist.github.com/ddctd143/03b92aed6c76986dbd99

Those two files extends laravel default builder, can be used with model that uses CustomModel.php instead of default laravel Model.php

@ddctd143 Will this work with Laravel 5.0?

I made the code by @ddctd143 work with Laravel 5.0 if anyone wondering:

Just change the code at line 36-37 from:

$queryType = $this->shouldRunExistsQuery($operator, $count) ? 'getRelationQuery' : 'getRelationCountQuery';

to:

$queryType = 'getRelationCountQuery';

Yes, I still use 5.0, for client's PHP version compatibility purposes. Laravel evolve so fast I can't expect all our client to have the latest PHP.

An easier workaround, without the need to change Laravel code, is to use $query->has('relation', '>', 0) instead of the (implicit) $query->has('relation', '>=', 1). That also avoids the exists query because shouldRunExistsQuery specifically checks for >= 1:

// From Illuminate\Database\Eloquent\Builder.php

protected function shouldRunExistsQuery($operator, $count)
{
    return ($operator === '>=' && $count === 1) || ($operator === '<' && $count === 1);
}

Note however that the performance will be bad! So to alleviate that, _first_ limit your result set and then use count instead of exists:

MyModel::has('relation', '>', 0)->where(...)->get(); // slow!
MyModel::where(...)->has('relation', '>', 0)->get(); // much faster.

Or, when you use scopes like function scopeHavingRelations():

MyModel::where(...)->havingRelations()->get();

@mk-relax This wont work.

I tried

$model = $model->has('advertable', '>', 0, 'and', function ($q) {
    $q->where('area', '>=', $this->fromArea);
});

And laravel does not know which polymorph table to use. It inserts laravel_reserved_0 instead of related table name. Attached error:

SQLSTATE[42S22]: Column not found: 1054 Unknown column 'area' in 'where clause' (SQL: select count(*) as aggregate from `adverts` where (select count(*) from `adverts` as `laravel_reserved_0` where `laravel_reserved_0`.`id` = `laravel_reserved_0`.`advertable_id` and `area` >= 40 and `laravel_reserved_0`.`deleted_at` is null) > 0 and `laravel_reserved_0`.`deleted_at` is null)

Still a no-fix?

@vjoao There won't be a fix as this issue was closed while ago.

+1

Is there an update for this?

I'm trying to filter on a polymorphic table.
Currently I’m able to filter through all the feeable objects…

    public function totalFees(){
        $total = 0;
        foreach ($this->fees as $fee){
            $total += $fee->feeable->fee;
        }
        return $total;
    }

but now I would like to only count the total fees from the feeable objects who only match either App\TuitionFee::class and App\FacilityFee::class.

I believe my question relates to this, right?

Did you mean filtering on relation? Have you look at constrain eager loading?

On 26 Sep 2016, at 7:58 PM, pavankataria <[email protected]notifications@github.com> wrote:

Is there an update for this?

I'm trying to filter on a polymorphic table.
Currently I’m able to filter through all the feeable objects…

public function totalFees(){
$total = 0;
foreach ($this->fees as $fee){
$total += $fee->feeable->fee;
}
return $total;
}
but now I would like to only count the total fees from the feeable objects who only match either App\TuitionFee::class and App\FacilityFee::class.

I believe my question relates to this, right?

—
You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHubhttps://github.com/laravel/framework/issues/5429#issuecomment-249550392, or mute the threadhttps://github.com/notifications/unsubscribe-auth/AAU5IX4Q3LYmwkjqL5awBIqnOYnFgbUZks5qt7OAgaJpZM4CWoDL.

If someone is having this issue, here is a workaround : perform a filter on the collection after the where requests. Using the example given by nkeena at the beginning of this thread :

return Transaction::where(...)->get()->filter(function ($value, $key) {
    return $value->transactionable->myField == 'myValue';
});

It doesn't do exactly the same thing as a whereHas would : it gets more Transaction items in database than necessary. But it works.

Should one even be allowed to execute whereHas() on a morphTo() relation while it is not working? I suggest to throw an exception for easier debugging.

And to make it work, could we add an array to the model that has the morphTo() relation to describe the possible classes it is linked to? This would enable us to add orWhereHas's for each entry in that array in the way @blankhua suggested. This would help at least some people. In case the array doesn't exist or doesn't have any entries for that relation, the exception I suggested above could be thrown.

If some people agree that this could be a viable solution, I can probably make a PR.

@GrahamCampbell Is anyone seeing my latest comment? Since the issue is closed. Or should I open a new one? I'd really like some feedback on it.

@JeroenVanOort why to restrict ? everything works with modifications provided here by others or by me in gists. I'm using morphTo with whereHas since Laravel 5.1

I think there should be a restriction when calling whereHas() on a morphTo() relation because it doesn't work, a least not out of the box. But, no restriction is needed when we can make it work, possibly the way I suggested. I think it would be a valuable addition if it can be made to work.

@JeroenVanOort can you submit PR to fix this issue, so that there is ability to use whereHas() on morphTo() ?

I think that restriction is not solution. After restriction would be made some of us, would need to hack code once more, so that temporary solution that is working for now would work again after restriction will be applied.

I came up with BelongsToMorph relationship type, and got whereHas working. Obviously, it only works on a single type, so you need to have explicit relationship declaration for each type.

// Model
class Comment extends Model
{
    public function post()
    {
        return BelongsToMorph::build($this, Post::class, 'commentable');
    }

    public function note()
    {
        return BelongsToMorph::build($this, Note::class, 'commentable');
    }
}

Source: https://gist.github.com/xxzefgh/3022fee8afa53e45a6b89da3f16b3815

It's very strange, that solution still is absent in official package. Hey, Taylor, want you think about this problem?

@dihalt This issue was closed while ago. I'm still using workaround code provided here by others, and modified by me. Laravel is now at version 5.4 and problem still exists. Maybe you could open new issue ?

Another workaround:

Add to Transaction.php

public function reservation()
    {
        return $this->blongsTo('Reservation::class');
    }
public function project()
    {
        return $this->blongsTo('Project::class');
    }

And do:

return Transaction::where('transactionable', 'reservation')->whereHas('reservation', function($q){
        return $q->where('id', '=', '1');
    })->get();

Pleeease

I came across this issue today in Laravel 5.5. I'll be joining the workaround crew for now.

Having had this problem a while ago, we wrote a composer package that extends the Eloquent Builder class and implements a work around for this issue. It's not perfect but it does the trick.

https://github.com/rapidwebltd/Improved-Polymorphic-Eloquent-Builder

It's currently designed for Laravel 5.1, but shouldn't be too hard to adapt for Laravel 5.5. Contributions welcome!

I searched how to fix where query with morphed table, and come to here and not solved yet :3rd_place_medal:

As a temporary workaround, you could declare a relationship for each type.

Transaction.php

    public function transactionable()
    {
        return $this->morphTo('transactionable');
    }
    public function reservation()
    {
        return $this->belongsTo(Reservation::class, 'transactionable_id')
            ->where('transactions.transactionable_type', Reservation::class);
    }
    public function project()
    {
        return $this->belongsTo(Project::class, 'transactionable_id')
            ->where('transactions.transactionable_type', Project::class);
    }

Reservation.php

    public function transactions()
    {
        return $this->morphMany(Transaction::class, 'transactionable');
    }

Project.php

    public function transactions()
    {
        return $this->morphMany(Transaction::class, 'transactionable');
    }

And then query the model like this:

    return Transaction::whereHas('reservation', function($q){
        return $q->where('id', '=', '1');
    })->orWhereHas('project', function($q){
        return $q->where('id', '=', '1');
    )->get();

It's not perfect but worked for me so I though I should share it in case someone has the same use case.

Just upgraded to Laravel 5.6, this bug is still present. Can please we re-open this issue?

Just ran into this issue today as well laravel 5.6.20

@flyrmyr this is a wont-fix. There is no non-hacky way of doing this.

Ran in the same problem today. If there is no fix for now maybe this could be mentioned in the documentation.

Same problem with 5.6

@pmochine

@thisdotvoid's solution here with BelongsToMorph seems to work without issue in 5.6.x

@Braunson yes it works but only for explicit relationships.

@thisdotvoid's solution works in Laravel 5.6.29 (implemented a few minutes ago). It will be nice to have an official way to do this in a close future :)

polymorphic relations.... same problem here! they are a pain in my ass...

Same problem here 5.7.9

Same here

This party never going to end..

Laravel 5.8.27 adds whereHasMorph(): #28928

woooooo ... i hardly can believe this! thank you so much

stops the warning but not result set
return $this->morphMany(MODEL::class, 'name','type','pk');

Was this page helpful?
0 / 5 - 0 ratings

Related issues

lzp819739483 picture lzp819739483  Â·  3Comments

jackmu95 picture jackmu95  Â·  3Comments

ghost picture ghost  Â·  3Comments

CupOfTea696 picture CupOfTea696  Â·  3Comments

SachinAgarwal1337 picture SachinAgarwal1337  Â·  3Comments