Damian Fraustro
Damian Fraustro

Reputation: 41

How to implement "related posts" in a HABTM relationship?

I use CakePHP 3.7.7, and I have a working belongsToMany association between my Posts and Tags models. In my Posts view I managed to list all the associated Tags via contain, and everything works as desired.

However, below the main post content I need to show some "related posts" suggestions.

I have been searching for an answer, and I think the solution might be by using matching, but I haven't been able to get the desired results for my query.

My Models:

class PostsTable extends Table
{
    public function initialize(array $config)
    {
        parent::initialize($config);

        $this->setTable('posts');
        $this->setDisplayField('name');
        $this->setPrimaryKey('id');

        $this->belongsToMany('Tags', [
            'foreignKey' => 'post_id',
            'targetForeignKey' => 'tag_id',
            'joinTable' => 'posts_tags',
    'dependant' => false
        ]);
    }
}

class TagsTable extends Table
{
    public function initialize(array $config)
    {
        parent::initialize($config);

        $this->setTable('Tags');
        $this->setDisplayField('title');
        $this->setPrimaryKey('id');

        $this->belongsTo('Tags', [
            'foreignKey' => 'tag_id',
            'joinType' => 'INNER'
        ]);

    $this->belongsToMany('Posts');
    }
}

class PostsTagsTable extends Table
{
    public function initialize(array $config)
    {
        parent::initialize($config);

        $this->setTable('posts_tags');
        $this->setDisplayField('id');
        $this->setPrimaryKey('id');

        $this->belongsTo('Posts', [
            'foreignKey' => 'post_id',
            'joinType' => 'INNER'
        ]);
        $this->belongsTo('Tags', [
            'foreignKey' => 'tag_id',
            'joinType' => 'INNER'
        ]);
    }
}

And my controller:

class PostsController extends AppController
{
    public function view($slug = null)
    {
        $post = $this->Posts->findBySlug($slug)
        ->contain(['Tags'])
        ->first();

        $this->set('post', $post);

    }
}

I tried adding this in my view function:

$relId = $post->Tags->id;

$related = $this->Posts->find('all')
    ->contain(['PostsTags','Tags'])
    ->matching('PostsTags', function(\Cake\ORM\Query $query) use ($post) {
        return $query->where([
            'PostsTags.tag_id' => $relPost
            ]);
        })
    ->limit(3)
    ->execute();

$this->set('relatedPosts', $related);

...but that doesn't work. I keep getting an error notice:

Notice (8): Trying to get property of non-object

So I'm obviously not being able to get a correct array with the tags ids corresponding to those related to the current post.

How can I make it work? Or what would a better alternative be?

Upvotes: 0

Views: 54

Answers (1)

ndm
ndm

Reputation: 60463

Assuming $post is a Post entity, there is no Tags property, so $post->Tags will return null, hence the error when you try to access the id property on the returned value.

By default the property name of a belongsToMany association is the pluralized, lower cased, underscored variant of the association name, so in your case tags. However it would be an array, so of course you cannot access an id property on it either.

If you want to find related posts based on the tags they share, then you'll either need a list of all tag IDs (not just a single one), or you have to make your query a little more complex and for example match against a subquery that fetches the currents posts tags. There's some other stuff wrong with your code, for example you don't have a concrete PostsTags association (so you can't contain or match against it), you're passing the wrong variable to the closure, you need to group by the posts primary key to avoid duplicates, and you probably want to exclude the post that you already have.

Here's a quick and dirty example using the already queried tags, first extract all IDs, then query the posts based on those IDs, excluding the current post:

$tagIds = collection($post->tags)->extract('id')->toArray();

if (!empty($tagIds)) {
    $relatedPosts = $this->Posts
        ->find()
        ->matching('Tags', function(\Cake\ORM\Query $query) use ($tagIds) {
            return $query->where([
                'Tags.id IN' => $tagIds
            ]);
        })
        ->where([
            'Posts.id !=' => $post->id
        ])
        ->group('Posts.id')
        ->limit(3);
} else {
    $relatedPosts = null;
}

$this->set('relatedPosts', $relatedPosts);

In your view you'd have to check whether $relatedPosts is null before working with it!

A subquery for fetching the tag IDs could for example look like this:

$tagIds = $this->Posts->Tags
    ->junction()
    ->find()
    ->select(['PostsTags.tag_id'])
    ->where([
        'PostsTags.post_id' => $post->id
    ]);

See also

Upvotes: 1

Related Questions