skip to Main Content

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');
    }
}

2

Answers


  1. Chosen as BEST ANSWER

    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()));
                }
            }
        }
    

  2. 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.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search