XF 2.2 Customizing "Similar threads"

Anatoliy

Well-known member
I use xenForo's "Similar threads" add-on and I like it. However there is a problem with it as it takes in consideration keyword relevance only, and doesn't (well, because it just can't) consider pages "authority". So I exported a list of top performing pages (bring most organic traffic) from Google Search Console, and my idea is to create "topic related clusters" of 11 pages, so each of them will have a widget with 10 links to others, and those 10 Similar threads will be not just keyword related, but also with the biggest "authority".

It was not difficult to create a first cluster, a widget with links to pages, and a conditional !$xf.visitor.user_id && in_array($xf.reply.contentKey, ['thread-x', 'thread-y', ...', 'thread-z']). And I added a conditional to the XFES: Similar threads widget so the pages that have the widget with my manually selected related links would not display the XFES: Similar threads widget. !$xf.visitor.user_id AND !in_array($xf.reply.contentKey, ['thread-x', ..., 'thread-z']). Everything seems to work as expected.

So here is a question that bothers me. To "similar link" all those 1000 best performing pages I will have to create 100 widgets. (And that's fine, I can handle it. )
So the conditional in XFES: Similar threads widget will include an array with 1000 :eek: elements.

Will it slow down page load speed for all the threads as php now will have to go through that huge array to see if XFES: Similar threads widget should be displayed?
 
sorry, my mistake. I used a wrong keyword column name. now it looks like it works, however when I
\XF::dump($keyword); it shows ''true", not the value "steelhead". )
and if I pass it to a template it shows '1'.

if I dump $thread I can see in values "av_lt_keyword" => "steelhead", how come
$keyword = $thread->av_lt_keyword; shows 1 and not steelhead?

I'm going sligtly maad...
Show how you added this column to the Thread entity structure.
 
Show how you added this column to the Thread entity structure.
Setup.php

PHP:
<?php

namespace AV\LinkedThreads;

use XF\AddOn\AbstractSetup;
use XF\AddOn\StepRunnerInstallTrait;
use XF\AddOn\StepRunnerUninstallTrait;
use XF\AddOn\StepRunnerUpgradeTrait;
use XF\Db\Schema\Alter;
use XF\Db\Schema\Create;

class Setup extends AbstractSetup
{
    use StepRunnerInstallTrait;
    use StepRunnerUpgradeTrait;
    use StepRunnerUninstallTrait;

    public function installStep1()
    {
        $this->schemaManager()->alterTable('xf_thread', function(Alter $table)
        {
            $table->addColumn('av_lt_keyword', 'varchar', 255);
        });
    }

    public function installStep2()
{
    $this->schemaManager()->createTable('av_linked_threads', function(Create $table)
    {
        $table->addColumn('id', 'int');
        $table->addColumn('title', 'varchar', 255);
        $table->addColumn('description', 'varchar', 255);
        $table->addColumn('keyword', 'varchar', 255);
        $table->addPrimaryKey('id');
    });
}

public function uninstallStep1()
{
    $this->schemaManager()->alterTable('xf_thread', function(Alter $table)
    {
        $table->dropColumns('av_stmanager_keyword');
    });
}

public function uninstallStep2()
{
    $this->schemaManager()->dropTable('av_stmanager');
}

}

Listener.php

Code:
<?php

namespace AV\LinkedThreads;

use XF\Mvc\Entity\Entity;

class Listener
{
    public static function threadEntityStructure(\XF\Mvc\Entity\Manager $em, \XF\Mvc\Entity\Structure &$structure)
    {
        $structure->columns['av_lt_keyword'] = ['type' => Entity::BOOL, 'default' => false];
        $structure->relations['LinkedThread'] = [
            'entity' => 'AV\LinkedThreads:LinkedThread',
            'type' => Entity::TO_ONE,
            'conditions' => 'id',
            'primary' => true
        ];
    }
}

Entity>LinkedThread.php

Code:
<?php

namespace AV\LinkedThreads\Entity;

use XF\Mvc\Entity\Structure;

class LinkedThread extends \XF\Mvc\Entity\Entity
{
    public static function getStructure(Structure $structure)
    {
        $structure->table = 'av_linked_threads';
        $structure->shortName = 'AV\LinkedThreads:LinkedThread';
        $structure->primaryKey = 'id';
        $structure->columns = [
            'id' => ['type' => self::UINT, 'required' => true],
            'keyword' => ['type' => self::STR, 'maxLength' => 255],
            'title' => ['type' => self::STR, 'maxLength' => 255],
            'description' => ['type' => self::STR, 'maxLength' => 255]
        ];
        $structure->getters = [];
        $structure->relations = [
            'Thread' => [
                'entity' => 'XF:Thread',
                'type' => self::TO_ONE,
                'conditions' => 'thread_id',
                'primary' => true
            ],
        ];
    
        return $structure;
    }
}
 
I got totally confused, but I guess my entity works - a widget displays threads with current keyword if I hard write a keyword in widget's finder
$data = $finder->where('keyword', 'salmon')->limit(5)->fetch();

I guess the problem is that I can't get a value from a custom column in xf_thread table. it returns 1. there are no such keywords so a widget returns nothing.
 
What would be the correct way to perform when a current thread has a keyword and should display linked threads, and not if doesn't?
Can I use in widget display condition something like $xf.thread.av_lt_keyword ? That would be cool. )))
Then I would put !$xf.thread.av_lt_keyword as condition to XF similar threads widget and I would be all set. But I feel like that probably will not be possible, right?
 
This won't work. But replacing $xf with $context should work.
Wow! It does work! Thank you!!!

I guess I'm almost there. The only thing - when I go to edit my widget it shows on the top

Template errors​

  • Template admin:widget_def_options_av_linked_threads: [E_USER_WARNING] Template admin:widget_def_options_av_linked_threads is unknown (src\XF\Template\Templater.php:689)
does it related to me not create step 3 in setup to create a widget definition, or what this def_options about?
 
Wow! It does work! Thank you!!!

I guess I'm almost there. The only thing - when I go to edit my widget it shows on the top

Template errors​

  • Template admin:widget_def_options_av_linked_threads: [E_USER_WARNING] Template admin:widget_def_options_av_linked_threads is unknown (src\XF\Template\Templater.php:689)
does it related to me not create step 3 in setup to create a widget definition, or what this def_options about?
Add a method to your widget class
PHP:
public function getOptionsTemplate()
{
    return null;
}
 
Fixed! Thank you!!!

So what's the deal with creating a widget position in setup (as in the tutorial)? I don't need it if I added a widget definition manually?
 
And also do not forget to filter the found threads in your widget
PHP:
<?php

namespace AV\LinkedThreads\Widget;

use XF\Widget\AbstractWidget;

class Widget extends AbstractWidget
{
    public function render()
    {
        $context = $this->getContextParams();
        $thread = $context['thread'];
        $keyword = $thread->keyword;
 
        $finder = $this->finder('AV\LinkedThreads:LinkedThread');
        $data = $finder->where('keyword', $keyword)->limit(5)->fetch();
       

        $viewParams = [
            'data' => $data,
        ];
        return $this->renderer('linked_threads_widget', $viewParams);
    }      

    public function getOptionsTemplate()
    {
        return null;
    }
}
 
So what's the deal with creating a widget position in setup (as in the tutorial)? I don't need it if I added a widget definition manually?
Don't mix up widget positions and definitions ;)

Both can be done in the admin cp. New positions have to be added to the template as well (Like in the Dev Tutorial: <xf:widgetpos id="demo_portal_view_sidebar" position="sidebar" />).

A setup is only needed if you want to apply default positions (AFAIK).
 
class LinkedThread extends \XF\Mvc\Entity\Entity
{
public static function getStructure(Structure $structure)
{
$structure->table = 'av_linked_threads';
$structure->shortName = 'AV\LinkedThreads:LinkedThread';
$structure->primaryKey = 'id';
$structure->columns = [
'id' => ['type' => self::UINT, 'required' => true],
'keyword' => ['type' => self::STR, 'maxLength' => 255],
'title' => ['type' => self::STR, 'maxLength' => 255],
'description' => ['type' => self::STR, 'maxLength' => 255]
];
$structure->getters = [];
$structure->relations = [
'Thread' => [
'entity' => 'XF:Thread',
'type' => self::TO_ONE,
'conditions' => 'thread_id',
'primary' => true
],
];

return $structure;
}
}
Your entity does not have a "thread_id" column for such a relation with "XF:Thread"
If the "id" column matches "thread_id", then

PHP:
'conditions' => [
    ['thread_id', '=', '$id']
]
 
Your entity does not have a "thread_id" column for such a relation with "XF:Thread"
If the "id" column matches "thread_id", then
I don't understand this one. But I'm all ready to learn. )

I thought that a widget will get current thread keyword and then fetch all threads titles and descriptions with this keyword from custom table. You mean that I don't need a custom table? You mean I should add description column to xf_thread and fetch all the threads with this keyword from xf_thread table?

Or you mean something totally different?)
 
Good catch by @DimmmCom

As far as I see, you don't use the relation in your code.

If you want to use a relation you have to define which columns must have the same values over the tables.

Basically a relation is needed if you want to use JOINs in the MySQL queries, that the entity manager builds.

See dev docs:


They define the relationship between entities which can be used to perform join queries to other tables or fetch records associated to an entity on the fly.

Anyhow, 1 keyword can be assigned to many threads, so the type would have to be a TO_MANY relation:

PHP:
'type' => self::TO_MANY,

And the condition would likely be keyword:

PHP:
'conditions' => 'keyword',

The TO_ONE relation would be in the other direction: If you extend the Thread entity and add relation to the keyword table.
 
Last edited:
As far as I see, you don't use the relation in your code.
I still don't understand if I need a relation.

First I was afraid to touch xf tables, so I decided that I will add an extra table with 4 columns - thread title, description, keyword and thread_id. Widget would get a current thread_id, then will find in a custom table the row with that thread_id, grab the corresponding keyword and then fetch all records with that keyword.

But then I understand that it will require 2 requests. So I added a "keyword" column to xf_thread table. Now I can grab current thread keyword and fetch from a custom table all records with that keyword. Just one request.

But today I understood that I can get the same without creating a custom table. I can just add one more column - thread_description to xf_thread.
And then I will grab current thread keyword and fetch from xf_thread all records with that keyword. Again just one request. Without relation, without extra table (that actually duplicates thread_id, thread_title, keyword; and only description is non duplicated data.

Do I see the picture right, or I got totally confused?
 
Do I see the picture right, or I got totally confused?
🤔 So, if you want to add 1 keyword to threads on demand and then find other threads with the same keyword, I guess no extra table is needed.

But don't forget to add an index to the keyword column (for performance reason).

Maybe you want to add such a relation to the Thread entity:

PHP:
'SameKeywordThreads' => [
    'entity' => 'XF:Thread',
    'type' => self::TO_MANY,
    'conditions' => 'keyword'
],

I never did that before, but I guess you can do then something like:

PHP:
$otherThreads = $thread->SameKeywordThreads;

Sorry, if that was nonsense.. :D
 
Top Bottom