Home > database >  How to get automatic id column name for polymorphic manyToMany relationship subquerying?
How to get automatic id column name for polymorphic manyToMany relationship subquerying?

Time:01-06

The below function allows me to retrieve related entries of a given model type that have, for instance, overlapping tags associated with them.

<?php

public function getRelated($entity, $fields = [], $relations = []) {
    $related = new EloquentCollection();

    // get models of same entity type that have relations in common

    if (count($relations)) {
        foreach ($relations as $relation) {
            // check if the current model in use has any of the currently iterated relationship at all
            if ($entity->{$relation}()->exists()) {
                // if yes, get all the ids of the related models, for instance tags
                $relationIds = $entity->{$relation}()->get()->pluck('id')->toArray();

                // now, get all same type models except the current one
                $relationEntities = $this->entity::whereNot('id', $entity->id)
                    // also except the ones we already found
                    ->whereNotIn('id', $related->modelKeys())
                    // and only get those which also have current relationship (e.g. tags)
                    ->whereHas($relation, function (Builder $query) use($relationIds, $relation) {
                        $query->whereIn($relation . '.id', $relationIds);
                    })
                    ->get()
                    ->sortKeysDesc()
                    ->take(10);

                $related = $related->concat($relationEntities->except($related->modelKeys()));
            }
        }
    }
}

This works well. So I could load a blog post, for instance, and show related blog posts that have some of the same tags by calling:

$post = Post::find(1);
$related = $this->getRelated($post, [], ['tags']);

Now, I also need to respect other types than tags, hence $relations is an array: "Show me related blog posts that have any of these tags, categories, you name it, in common."

My first problem was in the subquery. I had to hardcode the id in the relation table:

$query->whereIn('id', $relationIds);
had to be
$query->whereIn($relation . '.id', $relationIds);

So now my problem is that $relation . '.id' doesnt work for all cases. The relationship fooBars() would expect foo_bar.id but looks for fooBar.id.

Is there no automatic way of Laravel automatically knowing the column name based on the relationship I am currently querying? If not, I must, for each relatioship I am querying, define the column name (of the id) of the relationship table. Is there something I am missing?

Or can I somehow load the relationship and look at the DB scheme? Whats best practice here?

One way could be to use snake case ($query->whereIn(Str::of($relation)->snake() . '.id', $relationIds);) but that feels vague and dirty.

All of these are polymorphic morphToMany/ morphedByMany.


Edit: For better understanding I'm adding an example of what my models look like. They all use default naming conventions. Again, I want to get related Posts that have either some Tags or FooBars in common.

<?php

class Post extends Entity
{
    public function tags()
    {
        return $this->morphToMany(Tag::class, 'taggable');
    }

    public function fooBars()
    {
        return $this->morphToMany(FooBar::class, 'foo_barable');
    }    
}

class Tag extends Entity
{
    public function posts()
    {
        return $this->morphedByMany(Post::class, 'taggable');
    }
}

class FooBar extends Entity
{
    public function posts()
    {
        return $this->morphedByMany(Post::class, 'foo_barable');
    }
}

Thanks to @KGG's answer I came up with the following:

<?php

if (count($relations)) {
    foreach ($relations as $relation) {
        if ($entity->{$relation}()->exists()) {
            $relationIds = $entity->{$relation}()->get()->modelKeys();
            $relatedClass = get_class($entity->$relation()->getRelated());
            $relationPrimaryKey = ($instance = new $relatedClass)->getQualifiedKeyName();

            $relationEntities = $this->entity::whereNot('id', $entity->id)
                ->whereNotIn('id', $related->modelKeys())
                ->whereHas($relation, function (Builder $query) use($relationIds, $relationPrimaryKey) {
                    $query->whereIn($relationPrimaryKey, $relationIds);
                })
                ->get()
                ->sortKeysDesc()
                ->take(10)
                ;

            $related = $related->concat($relationEntities->except($related->modelKeys()));
        }
    }
}

CodePudding user response:

I made a mini version of your code to show concept: You can build on it to load inner relationships or do changes to relationships that are loaded.

$post = Post::first();
$related = $this->getRelated($post, ['tags', 'tags', 'tags', 'brands', 'somethingStrange']);

The getRelated function:

public function getRelated($entity, $relations = []) {
    if (count($relations)) {
        array_unique($relations);
    
        $verifiedRelationships = [];
        $verifiedRelationshipsPrimaryKey = [];
    
        foreach ($relations as $relation) {
            //If this method exists in the parent class continue
            if(method_exists($entity, $relation)) {
                //add the relation
                array_push($verifiedRelationships, $relation);
                //add the relation primary_id
                array_push($verifiedRelationshipsPrimaryKey, Str::lower(Str::singular(Str::snake(Str::studly($relation)))).'_id');
            }
        }
        
        $entity->load($verifiedRelationships);
    }
}

This will automatically load all related methods that are found in that class.

Few notes:

  1. $verifiedRelationships will return any found methods ['tags', 'brands', 'somethingStrange']
  2. $verifiedRelationshipsPrimaryKey will return ['tag_id', 'brand_id', 'something_strange_id']
  3. Laravel has a function on the model that gets the key using getKeyName();
  4. Laravel has a function on the model to return the qualified relationship id using getQualifiedKeyName()

Example of $model Primary Key inside the foreach loop:

$relatedClass = get_class($entity->$relation()->getRelated());
                    
$primaryKey = ($instance = new $relatedClass)->getKeyName();

//returns "id"
            

Example of the $relationship Primary key inside the foreach loop:

$relatedClass = get_class($entity->$relation()->getRelated());
                    
$primaryKey = ($instance = new $relatedClass)->getQualifiedKeyName();

//returns "post_brands.id" 

Please test this out, and if you want changes or have other cases for relationships or clarifications, I would be happy to adjust the code until it works for you, I honestly don't know the relationships you have, it would be easier if I can get an array example.

  • Related